@mercury-fw/channel-google-chat 0.8.1 → 0.9.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 +30 -0
- package/README.md +5 -1
- package/dist/google-chat-provider.d.ts +25 -7
- package/dist/messages.d.ts +10 -0
- package/google-chat-provider.ts +166 -64
- package/index.ts +5 -1
- package/messages.ts +30 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @mercury-fw/channel-google-chat
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e00ff73: The channels' texts come out in the person's language too, and the language their client asks for counts. Channel contract version 6:
|
|
8
|
+
|
|
9
|
+
- The person's language is the one they saved, else the one their client asks for, else the app's default. The HTTP channel passes on the request's `Accept-Language` header (what browsers send), Google Chat the language of the person's Chat client when its event carries one (`InboundTurn.languageHint`). Someone the agent refuses gets it too.
|
|
10
|
+
- A channel declares its own texts in English in `messages`, under its name (`google-chat.`, `http.`), checked like a plugin's; it gets them through `ctx.i18n.forRequest(principal, hint)`.
|
|
11
|
+
- The replies to a confirmation are the core's, in the person's language: `ctx.confirm` returns `{ text, resume? }` and `ctx.resolveConfirmation` the outcome with its `text`. A confirmed action reads "Confirmed and done." instead of its raw result as JSON. `confirmReply`, `RESUME_AFTER_CONFIRM` and `PENDING_CONFIRMATION_NOTE` are gone from the contract, and `tryConfirm` from `@mercury-fw/confirm-engine` (`resolveConfirmation` reports the outcome; the core words it).
|
|
12
|
+
- The stand-in for an answer the model didn't write because the turn staged an action (`core.pending.note`, "Action pending, waiting for confirmation.") is no longer streamed as text: `finalize(text, { pendingNote })` says the text begins with it, and channels drop it through that, never by comparing text.
|
|
13
|
+
- A plain staging's single choice has an empty label: each channel words its own button ("Confirm" on Google Chat, in the person's language). `loginLinkText(login, t)` takes the person's texts.
|
|
14
|
+
- HTTP: `Accept-Language` on `/turn`, `/confirm` and `/login/callback`; `Content-Language` on the responses of `/turn` and `/confirm`; the `final` event carries `pendingNote`; the login page reads in the browser's language.
|
|
15
|
+
- Google Chat: the cards' titles, status lines and confirm button are `google-chat.*` texts; the client's `common.userLocale` is the hint when the event carries it.
|
|
16
|
+
- The terminal's replies to a confirmation read in the terminal person's language, like its other lines.
|
|
17
|
+
|
|
18
|
+
The texts that were in Italian (the cards, the replies to a confirmation, the pending note) are now English by default. An app that wants them in Italian adds their keys to `locales/it.json` (`core.pending.note`, `core.confirm.*`, `core.login.link*`, `google-chat.*`, `http.*`; the log lists the missing ones at startup). A UI on the HTTP channel can hide the pending note through `final`'s `pendingNote`, and words the button of a choice with an empty label itself. A channel written for contract 5 isn't loaded by this core, and the log says why: rebuild it against `CHANNEL_API_VERSION` 6.
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- 6bb8291: Needs the framework at 0.54.0 or later, the version that brings the contract it's written for (plugin contract 5, channel contract 6): the peer range on `@mercury-fw/plugin-types` and `@mercury-fw/cli-engine` (the plugins) or `@mercury-fw/channel-types` (the channels) starts there.
|
|
23
|
+
|
|
24
|
+
## 0.8.2
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- 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.
|
|
29
|
+
- `@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.
|
|
30
|
+
- `@mercury-fw/channel-types`: `confirmReply(outcome)`, the reply to each confirmation outcome, for a channel that resolves a token through `resolveConfirmation`.
|
|
31
|
+
- `@mercury-fw/confirm-engine`: `tryConfirm` answers with `confirmReply`, same texts as before.
|
|
32
|
+
|
|
3
33
|
## 0.8.1
|
|
4
34
|
|
|
5
35
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -26,9 +26,13 @@ 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
|
+
This version is written for version 6 of the channel contract, the one that brings the channels' texts in the person's language, and needs the framework at 0.54 or later: a core with an older contract skips it, and the log says why.
|
|
30
|
+
|
|
29
31
|
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
32
|
|
|
31
|
-
|
|
33
|
+
Every text the channel shows a person (the cards' titles and status lines, the confirm button, the replies to a confirmation) is in their language: the one they asked the assistant for, else the language of their Chat client when the event carries it, else the app's default. The cards' own texts are `google-chat.` keys an app translates in its language files.
|
|
34
|
+
|
|
35
|
+
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 "Confirm" for a plain staging (deleting a Jira issue, say), more when there's a choice to make (the web search plugin offers "Only this time" and "Always for \<host\>", in the person's language, 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
36
|
|
|
33
37
|
## Table of contents
|
|
34
38
|
|
|
@@ -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
|
|
24
|
+
import { type ChannelI18n, type ChannelRuntimeContext, type Provider } 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
|
|
@@ -51,30 +51,48 @@ export type ParsedMessageEvent = {
|
|
|
51
51
|
* live against a real DM event this session.
|
|
52
52
|
*/
|
|
53
53
|
isDirectMessage: boolean;
|
|
54
|
+
/** The language of the sender's Chat client (`common.userLocale`), when the event carries it: the turn's language hint. */
|
|
55
|
+
locale?: string;
|
|
54
56
|
};
|
|
55
57
|
export type ParsedCardClickEvent = {
|
|
56
58
|
kind: "card-click";
|
|
59
|
+
/** The message holding the clicked card, when the event names it. */
|
|
60
|
+
messageName?: string;
|
|
57
61
|
space: string;
|
|
58
62
|
sender: string;
|
|
59
63
|
senderEmail: string | undefined;
|
|
60
64
|
parameters: Record<string, string>;
|
|
61
65
|
/** As on a message (see `ParsedMessageEvent.isDirectMessage`): for the turn a click resumes. */
|
|
62
66
|
isDirectMessage: boolean;
|
|
67
|
+
/** As on a message (see `ParsedMessageEvent.locale`). */
|
|
68
|
+
locale?: string;
|
|
63
69
|
};
|
|
64
70
|
/** Parses one decoded Pub/Sub event, or `null` if it isn't a kind this provider acts on. */
|
|
65
71
|
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
|
|
72
|
+
export type CardClickHandler = (params: Record<string, string>, space: string, sender: string, senderEmail?: string, isDirectMessage?: boolean, messageName?: string,
|
|
73
|
+
/** The clicking person's client language (`ParsedCardClickEvent.locale`). */
|
|
74
|
+
locale?: string) => Promise<void>;
|
|
67
75
|
export type GoogleChatProviderDeps = {
|
|
68
76
|
credentials: ServiceAccountCredentials;
|
|
69
77
|
subscription: string;
|
|
70
78
|
/**
|
|
71
79
|
* Resolves a confirmation token against the core's shared store, injected by
|
|
72
|
-
* the channel loader (`ChannelRuntimeContext.confirm`). Returns
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
80
|
+
* the channel loader (`ChannelRuntimeContext.confirm`). Returns what to tell
|
|
81
|
+
* the person, in their language, or `null` when the input wasn't
|
|
82
|
+
* token-shaped. With
|
|
83
|
+
* `resolveConfirmation` below, the channel's whole contact with confirmation:
|
|
84
|
+
* the store, vault and note-writer stay in the core.
|
|
76
85
|
*/
|
|
77
|
-
confirm:
|
|
86
|
+
confirm: ChannelRuntimeContext["confirm"];
|
|
87
|
+
/**
|
|
88
|
+
* The structured sibling of `confirm` (`ChannelRuntimeContext.resolveConfirmation`).
|
|
89
|
+
* When it's there, a click resolves through it, and an accepted choice
|
|
90
|
+
* turns its card into the choice taken; without it, a click goes through
|
|
91
|
+
* `confirm` and the card stays as it was.
|
|
92
|
+
*/
|
|
93
|
+
resolveConfirmation?: ChannelRuntimeContext["resolveConfirmation"];
|
|
94
|
+
/** The texts the cards show, in each person's language (`ChannelRuntimeContext.i18n`); English when absent. */
|
|
95
|
+
i18n?: ChannelI18n;
|
|
78
96
|
/**
|
|
79
97
|
* Handles a `CARD_CLICKED` event's action parameters. Defaults to resolving
|
|
80
98
|
* the confirm button's token through `deps.confirm` (the same path a bare
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Google Chat channel's texts a person sees, in English: the base every
|
|
3
|
+
* instance's language files translate (`ChannelPlugin.messages`). The cards'
|
|
4
|
+
* titles and status lines, the confirm button of a plain staging; the replies
|
|
5
|
+
* and the tool labels are the core's and the plugins'.
|
|
6
|
+
*/
|
|
7
|
+
import type { Messages, Translate } from "@mercury-fw/channel-types";
|
|
8
|
+
export declare const GOOGLE_CHAT_MESSAGES: Messages;
|
|
9
|
+
/** These texts in English, for a provider built without the core's (`ChannelI18n`); any other key comes back as itself. */
|
|
10
|
+
export declare const englishChat: Translate;
|
package/google-chat-provider.ts
CHANGED
|
@@ -35,9 +35,11 @@ import {
|
|
|
35
35
|
detectLoginRequired,
|
|
36
36
|
detectPendingConfirmation,
|
|
37
37
|
loginLinkText,
|
|
38
|
-
PENDING_CONFIRMATION_NOTE,
|
|
39
|
-
RESUME_AFTER_CONFIRM,
|
|
40
38
|
NO_REPLY,
|
|
39
|
+
type ChannelI18n,
|
|
40
|
+
type ChannelRuntimeContext,
|
|
41
|
+
type ConfirmReply,
|
|
42
|
+
type Translate,
|
|
41
43
|
type LoginRequest,
|
|
42
44
|
type PendingConfirmation,
|
|
43
45
|
type ToolOutcome,
|
|
@@ -46,18 +48,22 @@ import {
|
|
|
46
48
|
type TurnSink,
|
|
47
49
|
type Principal,
|
|
48
50
|
} from "@mercury-fw/channel-types";
|
|
51
|
+
import { englishChat } from "./messages.ts";
|
|
49
52
|
|
|
50
53
|
/**
|
|
51
54
|
* Builds the confirmation card sent when a step stages an irreversible
|
|
52
55
|
* command (see `pending-confirmation.ts`), one button per choice it offers
|
|
53
|
-
* (a single
|
|
54
|
-
* the token, so a click routes straight into
|
|
56
|
+
* (a single "Confirm" for a plain staging, in the person's language: `t`).
|
|
57
|
+
* Each button's parameters carry the token, so a click routes straight into
|
|
58
|
+
* the same confirmation path
|
|
55
59
|
* 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
|
|
60
|
+
* case, below) — the user never has to see or type the token themselves —
|
|
61
|
+
* plus its label and the summary, so the click can turn the card into
|
|
62
|
+
* `buildChosenCard` without the channel keeping any state.
|
|
57
63
|
*/
|
|
58
|
-
function buildConfirmCard(pending: PendingConfirmation): ChatCard {
|
|
64
|
+
function buildConfirmCard(pending: PendingConfirmation, t: Translate): ChatCard {
|
|
59
65
|
return {
|
|
60
|
-
header: { title: "
|
|
66
|
+
header: { title: t("google-chat.confirm.title") },
|
|
61
67
|
sections: [
|
|
62
68
|
{
|
|
63
69
|
widgets: [
|
|
@@ -65,8 +71,17 @@ function buildConfirmCard(pending: PendingConfirmation): ChatCard {
|
|
|
65
71
|
{
|
|
66
72
|
buttonList: {
|
|
67
73
|
buttons: pending.choices.map((choice) => ({
|
|
68
|
-
text: choice.label,
|
|
69
|
-
onClick: {
|
|
74
|
+
text: choice.label || t("google-chat.confirm.button"),
|
|
75
|
+
onClick: {
|
|
76
|
+
action: {
|
|
77
|
+
function: "confirm",
|
|
78
|
+
parameters: [
|
|
79
|
+
{ key: "token", value: choice.token },
|
|
80
|
+
{ key: "label", value: choice.label || t("google-chat.confirm.button") },
|
|
81
|
+
{ key: "summary", value: pending.summary },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
70
85
|
})),
|
|
71
86
|
},
|
|
72
87
|
},
|
|
@@ -76,19 +91,32 @@ function buildConfirmCard(pending: PendingConfirmation): ChatCard {
|
|
|
76
91
|
};
|
|
77
92
|
}
|
|
78
93
|
|
|
94
|
+
/** The confirmation card once a choice is taken: the same request, the choice instead of the buttons. */
|
|
95
|
+
function buildChosenCard(summary: string, label: string | undefined, t: Translate): ChatCard {
|
|
96
|
+
return {
|
|
97
|
+
header: { title: t("google-chat.confirm.title") },
|
|
98
|
+
sections: [
|
|
99
|
+
{
|
|
100
|
+
widgets: [
|
|
101
|
+
{ textParagraph: { text: `\`${summary}\`` } },
|
|
102
|
+
{ textParagraph: { text: label === undefined ? t("google-chat.confirm.taken") : t("google-chat.confirm.chosen", { label }) } },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
79
109
|
/** 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 {
|
|
110
|
+
function buildLoginCard(login: LoginRequest, t: Translate): ChatCard {
|
|
81
111
|
return {
|
|
82
|
-
sections: [{ widgets: [{ buttonList: { buttons: [{ text: loginLinkText(login), onClick: { openLink: { url: login.url } } }] } }] }],
|
|
112
|
+
sections: [{ widgets: [{ buttonList: { buttons: [{ text: loginLinkText(login, t), onClick: { openLink: { url: login.url } } }] } }] }],
|
|
83
113
|
};
|
|
84
114
|
}
|
|
85
115
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
pending: "In attesa di conferma.",
|
|
91
|
-
};
|
|
116
|
+
/** A card's status line, in the person's language. */
|
|
117
|
+
function statusLine(status: "loading" | ToolOutcome, t: Translate): string {
|
|
118
|
+
return t(`google-chat.status.${status}`);
|
|
119
|
+
}
|
|
92
120
|
|
|
93
121
|
/**
|
|
94
122
|
* A tool-call/reasoning status card: `label` is a section `header`
|
|
@@ -101,10 +129,10 @@ const TOOL_STATUS_LINES: Record<"loading" | ToolOutcome, string> = {
|
|
|
101
129
|
* message.
|
|
102
130
|
*
|
|
103
131
|
* The status is appended to the title too, not just left inside the
|
|
104
|
-
* collapsed body: a title stuck reading "
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
132
|
+
* collapsed body: a title stuck reading "Reading Jira…" (present
|
|
133
|
+
* progressive) forever, even once the card's own body says "Done.", looked
|
|
134
|
+
* like the card never updated at all — confirmed live (the body did patch,
|
|
135
|
+
* only the title didn't change).
|
|
108
136
|
*
|
|
109
137
|
* Native `collapsible` state is client-side only and resets to collapsed
|
|
110
138
|
* on every PATCH — confirmed live. Harmless for a card patched exactly
|
|
@@ -118,14 +146,14 @@ const TOOL_STATUS_LINES: Record<"loading" | ToolOutcome, string> = {
|
|
|
118
146
|
* nothing patches the message again, so the native toggle can never be
|
|
119
147
|
* reset out from under the user again either.
|
|
120
148
|
*/
|
|
121
|
-
function buildToolCallCard(label: string, detail: string, status: "loading" | ToolOutcome): ChatCard {
|
|
149
|
+
function buildToolCallCard(label: string, detail: string, status: "loading" | ToolOutcome, t: Translate): ChatCard {
|
|
122
150
|
return {
|
|
123
151
|
sections: [
|
|
124
152
|
{
|
|
125
|
-
header: status === "loading" ? label : `${label} ${
|
|
153
|
+
header: status === "loading" ? label : `${label} ${statusLine(status, t)}`,
|
|
126
154
|
collapsible: true,
|
|
127
155
|
uncollapsibleWidgetsCount: 0,
|
|
128
|
-
widgets: [{ textParagraph: { text: detail } }, { textParagraph: { text:
|
|
156
|
+
widgets: [{ textParagraph: { text: detail } }, { textParagraph: { text: statusLine(status, t) } }],
|
|
129
157
|
},
|
|
130
158
|
],
|
|
131
159
|
};
|
|
@@ -143,7 +171,7 @@ const REASONING_TAIL_CHARS = 4000;
|
|
|
143
171
|
* any intermediate loading/prefill event to distinguish finer-grained
|
|
144
172
|
* states here (checked against the API docs) — `load_duration`/
|
|
145
173
|
* `prompt_eval_duration` only appear in the final, non-streamed response.
|
|
146
|
-
* Header is the generic "
|
|
174
|
+
* Header is the generic "Status" (not a restatement of the body) so this
|
|
147
175
|
* has somewhere to grow if a genuinely different state ever becomes
|
|
148
176
|
* observable, rather than needing a redesign.
|
|
149
177
|
*
|
|
@@ -152,12 +180,12 @@ const REASONING_TAIL_CHARS = 4000;
|
|
|
152
180
|
* (confirmed live — a thin grey line, no visible text at all), so this
|
|
153
181
|
* always carries at least one `textParagraph`, same as every other card
|
|
154
182
|
* in this file. If the turn's first reasoning burst arrives,
|
|
155
|
-
* `onReasoningChunk` patches this exact card into the "
|
|
183
|
+
* `onReasoningChunk` patches this exact card into the "Thinking…"
|
|
156
184
|
* card in place, rather than sending a second message; otherwise it's
|
|
157
185
|
* simply left as the last status before the turn's real answer.
|
|
158
186
|
*/
|
|
159
|
-
function buildReceivingCard(): ChatCard {
|
|
160
|
-
return { sections: [{ header: "
|
|
187
|
+
function buildReceivingCard(t: Translate): ChatCard {
|
|
188
|
+
return { sections: [{ header: t("google-chat.receiving.title"), widgets: [{ textParagraph: { text: t("google-chat.receiving.text") } }] }] };
|
|
161
189
|
}
|
|
162
190
|
|
|
163
191
|
/**
|
|
@@ -198,6 +226,8 @@ type RawChatEvent = {
|
|
|
198
226
|
action?: { actionMethodName?: string; parameters?: Array<{ key?: string; value?: string }> };
|
|
199
227
|
space?: { name?: string; type?: string };
|
|
200
228
|
user?: RawChatSender;
|
|
229
|
+
/** What the person's Chat client says about them; `userLocale` is their language (`en-US`), when the event carries it. */
|
|
230
|
+
common?: { userLocale?: string };
|
|
201
231
|
};
|
|
202
232
|
|
|
203
233
|
export type ParsedMessageEvent = {
|
|
@@ -217,18 +247,30 @@ export type ParsedMessageEvent = {
|
|
|
217
247
|
* live against a real DM event this session.
|
|
218
248
|
*/
|
|
219
249
|
isDirectMessage: boolean;
|
|
250
|
+
/** The language of the sender's Chat client (`common.userLocale`), when the event carries it: the turn's language hint. */
|
|
251
|
+
locale?: string;
|
|
220
252
|
};
|
|
221
253
|
|
|
222
254
|
export type ParsedCardClickEvent = {
|
|
223
255
|
kind: "card-click";
|
|
256
|
+
/** The message holding the clicked card, when the event names it. */
|
|
257
|
+
messageName?: string;
|
|
224
258
|
space: string;
|
|
225
259
|
sender: string;
|
|
226
260
|
senderEmail: string | undefined;
|
|
227
261
|
parameters: Record<string, string>;
|
|
228
262
|
/** As on a message (see `ParsedMessageEvent.isDirectMessage`): for the turn a click resumes. */
|
|
229
263
|
isDirectMessage: boolean;
|
|
264
|
+
/** As on a message (see `ParsedMessageEvent.locale`). */
|
|
265
|
+
locale?: string;
|
|
230
266
|
};
|
|
231
267
|
|
|
268
|
+
/** The language the event says the person's client is in, when it says. */
|
|
269
|
+
function localeOf(event: RawChatEvent | null | undefined): string | undefined {
|
|
270
|
+
const locale = event?.common?.userLocale;
|
|
271
|
+
return typeof locale === "string" && locale.trim() !== "" ? locale.trim() : undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
232
274
|
/** The email Google vouches for on `sender`: a person's only (`HUMAN`), never a bot's or an unknown kind's. */
|
|
233
275
|
function emailOf(sender: RawChatSender | undefined): string | undefined {
|
|
234
276
|
return typeof sender?.email === "string" && sender.email !== "" && sender.type === "HUMAN" ? sender.email : undefined;
|
|
@@ -237,6 +279,7 @@ function emailOf(sender: RawChatSender | undefined): string | undefined {
|
|
|
237
279
|
/** Parses one decoded Pub/Sub event, or `null` if it isn't a kind this provider acts on. */
|
|
238
280
|
export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardClickEvent | null {
|
|
239
281
|
const event = raw as RawChatEvent;
|
|
282
|
+
const locale = localeOf(event);
|
|
240
283
|
if (event?.type === "MESSAGE") {
|
|
241
284
|
const m = event.message;
|
|
242
285
|
if (!m || typeof m.name !== "string" || typeof m.text !== "string") return null;
|
|
@@ -251,6 +294,7 @@ export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardCli
|
|
|
251
294
|
senderDisplayName: m.sender.displayName,
|
|
252
295
|
senderEmail: emailOf(m.sender),
|
|
253
296
|
isDirectMessage: m.space.type === "DM",
|
|
297
|
+
...(locale === undefined ? {} : { locale }),
|
|
254
298
|
};
|
|
255
299
|
}
|
|
256
300
|
if (event?.type === "CARD_CLICKED") {
|
|
@@ -264,11 +308,13 @@ export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardCli
|
|
|
264
308
|
}
|
|
265
309
|
return {
|
|
266
310
|
kind: "card-click",
|
|
311
|
+
...(typeof event.message?.name === "string" ? { messageName: event.message.name } : {}),
|
|
267
312
|
space: event.space.name,
|
|
268
313
|
sender: event.user.name,
|
|
269
314
|
senderEmail: emailOf(event.user),
|
|
270
315
|
parameters,
|
|
271
316
|
isDirectMessage: event.space.type === "DM",
|
|
317
|
+
...(locale === undefined ? {} : { locale }),
|
|
272
318
|
};
|
|
273
319
|
}
|
|
274
320
|
return null;
|
|
@@ -280,6 +326,9 @@ export type CardClickHandler = (
|
|
|
280
326
|
sender: string,
|
|
281
327
|
senderEmail?: string,
|
|
282
328
|
isDirectMessage?: boolean,
|
|
329
|
+
messageName?: string,
|
|
330
|
+
/** The clicking person's client language (`ParsedCardClickEvent.locale`). */
|
|
331
|
+
locale?: string,
|
|
283
332
|
) => Promise<void>;
|
|
284
333
|
|
|
285
334
|
export type GoogleChatProviderDeps = {
|
|
@@ -287,12 +336,22 @@ export type GoogleChatProviderDeps = {
|
|
|
287
336
|
subscription: string;
|
|
288
337
|
/**
|
|
289
338
|
* Resolves a confirmation token against the core's shared store, injected by
|
|
290
|
-
* the channel loader (`ChannelRuntimeContext.confirm`). Returns
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
339
|
+
* the channel loader (`ChannelRuntimeContext.confirm`). Returns what to tell
|
|
340
|
+
* the person, in their language, or `null` when the input wasn't
|
|
341
|
+
* token-shaped. With
|
|
342
|
+
* `resolveConfirmation` below, the channel's whole contact with confirmation:
|
|
343
|
+
* the store, vault and note-writer stay in the core.
|
|
344
|
+
*/
|
|
345
|
+
confirm: ChannelRuntimeContext["confirm"];
|
|
346
|
+
/**
|
|
347
|
+
* The structured sibling of `confirm` (`ChannelRuntimeContext.resolveConfirmation`).
|
|
348
|
+
* When it's there, a click resolves through it, and an accepted choice
|
|
349
|
+
* turns its card into the choice taken; without it, a click goes through
|
|
350
|
+
* `confirm` and the card stays as it was.
|
|
294
351
|
*/
|
|
295
|
-
|
|
352
|
+
resolveConfirmation?: ChannelRuntimeContext["resolveConfirmation"];
|
|
353
|
+
/** The texts the cards show, in each person's language (`ChannelRuntimeContext.i18n`); English when absent. */
|
|
354
|
+
i18n?: ChannelI18n;
|
|
296
355
|
/**
|
|
297
356
|
* Handles a `CARD_CLICKED` event's action parameters. Defaults to resolving
|
|
298
357
|
* the confirm button's token through `deps.confirm` (the same path a bare
|
|
@@ -327,6 +386,9 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
327
386
|
const subscriptionFn = deps.subscriptionFn ?? openSubscription;
|
|
328
387
|
const clientDeps = { tokenSource };
|
|
329
388
|
const sentMessageNames = new Set<string>();
|
|
389
|
+
/** The texts for `principal`, on the client language `hint`. */
|
|
390
|
+
const textsFor = async (principal: Principal, hint: string | undefined): Promise<Translate> =>
|
|
391
|
+
deps.i18n ? (await deps.i18n.forRequest(principal, hint)).t : englishChat;
|
|
330
392
|
// Per-session serialization: pollOnce's setInterval fires on a fixed
|
|
331
393
|
// clock regardless of whether the previous tick's turn finished, so two
|
|
332
394
|
// overlapping ticks could otherwise both call handleTurn for the SAME
|
|
@@ -360,9 +422,16 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
360
422
|
}
|
|
361
423
|
}
|
|
362
424
|
|
|
363
|
-
/** Resumes the session's turn after a confirmation whose result went back to the model (`
|
|
364
|
-
function resumeTurn(
|
|
425
|
+
/** Resumes the session's turn after a confirmation whose result went back to the model (`ConfirmReply.resume`). */
|
|
426
|
+
async function resumeTurn(
|
|
427
|
+
handleTurn: HandleTurn,
|
|
428
|
+
space: string,
|
|
429
|
+
principal: Principal,
|
|
430
|
+
isDirectMessage: boolean,
|
|
431
|
+
locale: string | undefined,
|
|
432
|
+
): Promise<void> {
|
|
365
433
|
const sessionKey = deriveSessionKey(space, principal.id);
|
|
434
|
+
const t = await textsFor(principal, locale);
|
|
366
435
|
return runInSession(sessionKey, () =>
|
|
367
436
|
handleTurn(
|
|
368
437
|
{
|
|
@@ -373,8 +442,9 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
373
442
|
principal,
|
|
374
443
|
logPrefix: `[chat:${space}:${principal.id}] `,
|
|
375
444
|
resumeAfterConfirm: true,
|
|
445
|
+
...(locale === undefined ? {} : { languageHint: locale }),
|
|
376
446
|
},
|
|
377
|
-
createSink(space, { user: principal.id, isDirectMessage }),
|
|
447
|
+
createSink(space, { user: principal.id, isDirectMessage }, t),
|
|
378
448
|
),
|
|
379
449
|
);
|
|
380
450
|
}
|
|
@@ -383,11 +453,14 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
383
453
|
* Default `onCardClick`: the confirm button's token routes through the exact
|
|
384
454
|
* same `deps.confirm` a bare token typed on the terminal uses — one execution
|
|
385
455
|
* path, one set of valid/expired/wrong-session-token behaviors, regardless of
|
|
386
|
-
* how the token got here.
|
|
456
|
+
* how the token got here. A click the core accepted (the action ran, failed,
|
|
457
|
+
* or the turn resumes) turns its card into the choice taken, before
|
|
458
|
+
* anything else; one it didn't (`not-found`: expired, used, or someone
|
|
459
|
+
* else's in a shared space) leaves the card for whoever has to choose.
|
|
387
460
|
*/
|
|
388
461
|
const onCardClick: CardClickHandler =
|
|
389
462
|
deps.onCardClick ??
|
|
390
|
-
(async (params, space, sender, senderEmail, isDirectMessage = false) => {
|
|
463
|
+
(async (params, space, sender, senderEmail, isDirectMessage = false, messageName, locale) => {
|
|
391
464
|
const token = params.token;
|
|
392
465
|
if (!token) {
|
|
393
466
|
log(`[chat] card click with no token parameter`);
|
|
@@ -398,14 +471,36 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
398
471
|
provider: "google-chat",
|
|
399
472
|
...(senderEmail === undefined ? {} : { claims: { email: senderEmail } }),
|
|
400
473
|
};
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
474
|
+
const sessionKey = deriveSessionKey(space, sender);
|
|
475
|
+
let reply: ConfirmReply | null;
|
|
476
|
+
if (deps.resolveConfirmation) {
|
|
477
|
+
const outcome = await deps.resolveConfirmation(token, sessionKey, principal, locale);
|
|
478
|
+
const accepted = outcome.status === "ok" || outcome.status === "failed" || outcome.status === "resume";
|
|
479
|
+
if (accepted && messageName !== undefined && params.summary !== undefined) {
|
|
480
|
+
try {
|
|
481
|
+
const t = await textsFor(principal, locale);
|
|
482
|
+
await updateCardFn(messageName, buildChosenCard(params.summary, params.label, t), clientDeps);
|
|
483
|
+
log(`[chat:${space}] [out] confirm card ${messageName} patched to the choice taken`);
|
|
484
|
+
} catch (err) {
|
|
485
|
+
log(`[chat:${space}] confirm card patch failed: ${String(err)}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// On not-found nothing was taken, so `confirm` gives the reply as it
|
|
489
|
+
// always did: the core's own message for someone it won't talk to,
|
|
490
|
+
// which the outcome doesn't carry.
|
|
491
|
+
if (outcome.status === "not-found") reply = await deps.confirm(token, sessionKey, principal, locale);
|
|
492
|
+
else if (outcome.text === null) reply = null;
|
|
493
|
+
else reply = outcome.status === "resume" ? { text: outcome.text, resume: true } : { text: outcome.text };
|
|
494
|
+
} else {
|
|
495
|
+
reply = await deps.confirm(token, sessionKey, principal, locale);
|
|
496
|
+
}
|
|
497
|
+
if (reply?.resume && activeHandleTurn) {
|
|
498
|
+
await resumeTurn(activeHandleTurn, space, principal, isDirectMessage, locale);
|
|
404
499
|
return;
|
|
405
500
|
}
|
|
406
501
|
if (reply !== null) {
|
|
407
|
-
log(`[chat:${space}] [out] ${reply}`);
|
|
408
|
-
const sent = await sendMessageFn(space, reply, clientDeps);
|
|
502
|
+
log(`[chat:${space}] [out] ${reply.text}`);
|
|
503
|
+
const sent = await sendMessageFn(space, reply.text, clientDeps);
|
|
409
504
|
sentMessageNames.add(sent.name);
|
|
410
505
|
}
|
|
411
506
|
});
|
|
@@ -415,8 +510,9 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
415
510
|
* Per-turn output sink — same responsibilities as the retired `ChatStreamer`,
|
|
416
511
|
* rebuilt against the new client. `viewer` is the person the turn is for;
|
|
417
512
|
* outside a direct message, what only they may see is sent to them alone.
|
|
513
|
+
* `t` gives the cards' texts in their language.
|
|
418
514
|
*/
|
|
419
|
-
function createSink(space: string, viewer: { user: string; isDirectMessage: boolean }): TurnSink {
|
|
515
|
+
function createSink(space: string, viewer: { user: string; isDirectMessage: boolean }, t: Translate): TurnSink {
|
|
420
516
|
let chain: Promise<void> = Promise.resolve();
|
|
421
517
|
|
|
422
518
|
function enqueue(fn: () => Promise<void>): void {
|
|
@@ -458,7 +554,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
458
554
|
const shownLogins = new Set<string>();
|
|
459
555
|
enqueue(async () => {
|
|
460
556
|
log(`[chat:${space}] [out] receiving card`);
|
|
461
|
-
const sent = await sendCardFn(space, buildReceivingCard(), clientDeps);
|
|
557
|
+
const sent = await sendCardFn(space, buildReceivingCard(t), clientDeps);
|
|
462
558
|
sentMessageNames.add(sent.name);
|
|
463
559
|
receivingCardName = sent.name;
|
|
464
560
|
});
|
|
@@ -470,7 +566,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
470
566
|
return;
|
|
471
567
|
}
|
|
472
568
|
enqueue(async () => {
|
|
473
|
-
const card = buildToolCallCard(label, detail ?? "", "loading");
|
|
569
|
+
const card = buildToolCallCard(label, detail ?? "", "loading", t);
|
|
474
570
|
log(`[chat:${space}] [out] tool card: ${label}`);
|
|
475
571
|
const sent = await sendCardFn(space, card, clientDeps);
|
|
476
572
|
sentMessageNames.add(sent.name);
|
|
@@ -484,7 +580,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
484
580
|
log(`[chat:${space}] onToolFinish for unknown toolCallId ${toolCallId}`);
|
|
485
581
|
return;
|
|
486
582
|
}
|
|
487
|
-
const card = buildToolCallCard(entry.label, entry.detail, outcome);
|
|
583
|
+
const card = buildToolCallCard(entry.label, entry.detail, outcome, t);
|
|
488
584
|
log(`[chat:${space}] [out] patching ${entry.name} to "${outcome}"`);
|
|
489
585
|
const patched = await updateCardFn(entry.name, card, clientDeps);
|
|
490
586
|
log(`[chat:${space}] [out] patched ${patched.name}`);
|
|
@@ -501,9 +597,9 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
501
597
|
entry = { name: undefined, buffer: chunk };
|
|
502
598
|
reasoningCards.set(id, entry);
|
|
503
599
|
enqueue(async () => {
|
|
504
|
-
const card = buildToolCallCard("
|
|
600
|
+
const card = buildToolCallCard(t("google-chat.thinking"), "", "loading", t);
|
|
505
601
|
if (receivingCardName !== undefined) {
|
|
506
|
-
// Replace the "
|
|
602
|
+
// Replace the "Status" placeholder in place instead of
|
|
507
603
|
// sending a second message.
|
|
508
604
|
log(`[chat:${space}] [out] reasoning card (${id}) — replacing receiving card`);
|
|
509
605
|
const patched = await updateCardFn(receivingCardName, card, clientDeps);
|
|
@@ -530,7 +626,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
530
626
|
enqueue(async () => {
|
|
531
627
|
if (entry.name === undefined) return; // shouldn't happen given enqueue's own FIFO ordering, but stays defensive
|
|
532
628
|
const status = failed ? "failed" : "success";
|
|
533
|
-
const card = buildToolCallCard("
|
|
629
|
+
const card = buildToolCallCard(t("google-chat.thinking"), tailTruncate(entry.buffer, REASONING_TAIL_CHARS), status, t);
|
|
534
630
|
log(`[chat:${space}] [out] reasoning patch (${id}): status=${status} bufferLen=${entry.buffer.length}`);
|
|
535
631
|
await updateCardFn(entry.name, card, clientDeps);
|
|
536
632
|
reasoningCards.delete(id);
|
|
@@ -546,7 +642,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
546
642
|
if (pending) {
|
|
547
643
|
enqueue(async () => {
|
|
548
644
|
log(`[chat:${space}] [out] confirm card: ${pending.summary}`);
|
|
549
|
-
const sent = await sendCardFn(space, buildConfirmCard(pending), clientDeps);
|
|
645
|
+
const sent = await sendCardFn(space, buildConfirmCard(pending, t), clientDeps);
|
|
550
646
|
sentMessageNames.add(sent.name);
|
|
551
647
|
});
|
|
552
648
|
}
|
|
@@ -556,14 +652,18 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
556
652
|
enqueue(async () => {
|
|
557
653
|
log(`[chat:${space}] [out] login card: ${login.service}`);
|
|
558
654
|
const options = viewer.isDirectMessage ? undefined : { privateViewer: viewer.user };
|
|
559
|
-
const sent = await sendCardFn(space, buildLoginCard(login), clientDeps, options);
|
|
655
|
+
const sent = await sendCardFn(space, buildLoginCard(login, t), clientDeps, options);
|
|
560
656
|
sentMessageNames.add(sent.name);
|
|
561
657
|
});
|
|
562
658
|
}
|
|
563
659
|
},
|
|
564
|
-
finalize: async (finalText
|
|
565
|
-
|
|
566
|
-
|
|
660
|
+
finalize: async (finalText, info) => {
|
|
661
|
+
// The core's stand-in for an answer the model didn't write: the
|
|
662
|
+
// confirmation card already says it; only a list after it is sent.
|
|
663
|
+
const note = info?.pendingNote;
|
|
664
|
+
const shown = note !== undefined && finalText.startsWith(note) ? finalText.slice(note.length) : finalText;
|
|
665
|
+
const trimmed = shown.trim();
|
|
666
|
+
if (trimmed.length > 0 && trimmed !== NO_REPLY) {
|
|
567
667
|
for (const message of splitForSendLimit(trimmed)) {
|
|
568
668
|
sendPlain(message);
|
|
569
669
|
}
|
|
@@ -590,19 +690,20 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
590
690
|
...(event.senderEmail === undefined ? {} : { claims: { email: event.senderEmail } }),
|
|
591
691
|
};
|
|
592
692
|
|
|
593
|
-
const
|
|
594
|
-
if (
|
|
595
|
-
await resumeTurn(handleTurn, event.space, principal, event.isDirectMessage);
|
|
693
|
+
const reply = await deps.confirm(event.text, sessionKey, principal, event.locale);
|
|
694
|
+
if (reply?.resume) {
|
|
695
|
+
await resumeTurn(handleTurn, event.space, principal, event.isDirectMessage, event.locale);
|
|
596
696
|
return;
|
|
597
697
|
}
|
|
598
|
-
if (
|
|
599
|
-
log(`[chat:${event.space}] [out] ${
|
|
600
|
-
const sent = await sendMessageFn(event.space,
|
|
698
|
+
if (reply !== null) {
|
|
699
|
+
log(`[chat:${event.space}] [out] ${reply.text}`);
|
|
700
|
+
const sent = await sendMessageFn(event.space, reply.text, clientDeps);
|
|
601
701
|
sentMessageNames.add(sent.name);
|
|
602
702
|
return;
|
|
603
703
|
}
|
|
604
704
|
|
|
605
|
-
|
|
705
|
+
const t = await textsFor(principal, event.locale);
|
|
706
|
+
// Only the actual model turn needs serializing — the confirmation above
|
|
606
707
|
// never touches SessionHistory, so it's always safe to run right
|
|
607
708
|
// away regardless of whether this session is mid-turn.
|
|
608
709
|
await runInSession(sessionKey, () =>
|
|
@@ -614,8 +715,9 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
614
715
|
sessionKey,
|
|
615
716
|
principal,
|
|
616
717
|
logPrefix: `[chat:${event.space}:${event.sender}] `,
|
|
718
|
+
...(event.locale === undefined ? {} : { languageHint: event.locale }),
|
|
617
719
|
},
|
|
618
|
-
createSink(event.space, { user: event.sender, isDirectMessage: event.isDirectMessage }),
|
|
720
|
+
createSink(event.space, { user: event.sender, isDirectMessage: event.isDirectMessage }, t),
|
|
619
721
|
),
|
|
620
722
|
);
|
|
621
723
|
}
|
|
@@ -648,7 +750,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
648
750
|
if (parsed.kind === "message") {
|
|
649
751
|
await processMessageEvent(parsed, handleTurn);
|
|
650
752
|
} else {
|
|
651
|
-
await onCardClick(parsed.parameters, parsed.space, parsed.sender, parsed.senderEmail, parsed.isDirectMessage);
|
|
753
|
+
await onCardClick(parsed.parameters, parsed.space, parsed.sender, parsed.senderEmail, parsed.isDirectMessage, parsed.messageName, parsed.locale);
|
|
652
754
|
}
|
|
653
755
|
} catch (err) {
|
|
654
756
|
log(`[chat] event handling failed: ${String(err)}`);
|
package/index.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { ChannelPlugin, ChannelRuntimeContext } from "@mercury-fw/channel-types";
|
|
11
11
|
import { createGoogleChatProvider } from "./google-chat-provider.ts";
|
|
12
|
+
import { GOOGLE_CHAT_MESSAGES } from "./messages.ts";
|
|
12
13
|
|
|
13
14
|
/** Reads a required env var, failing loudly (caught by the channel loader) instead of silently degrading. */
|
|
14
15
|
function require(env: ChannelRuntimeContext["env"], name: string): string {
|
|
@@ -22,8 +23,9 @@ function require(env: ChannelRuntimeContext["env"], name: string): string {
|
|
|
22
23
|
export const googleChatChannel: ChannelPlugin = {
|
|
23
24
|
// The contract this channel is written for, as a literal: importing
|
|
24
25
|
// CHANNEL_API_VERSION would report whichever contract is installed.
|
|
25
|
-
apiVersion:
|
|
26
|
+
apiVersion: 6,
|
|
26
27
|
name: "google-chat",
|
|
28
|
+
messages: GOOGLE_CHAT_MESSAGES,
|
|
27
29
|
build: (ctx) => {
|
|
28
30
|
// No subscription configured ⇒ this instance simply doesn't run Google Chat.
|
|
29
31
|
const subscription = ctx.env.GOOGLE_CHAT_PUBSUB_SUBSCRIPTION;
|
|
@@ -39,6 +41,8 @@ export const googleChatChannel: ChannelPlugin = {
|
|
|
39
41
|
},
|
|
40
42
|
subscription,
|
|
41
43
|
confirm: ctx.confirm,
|
|
44
|
+
...(ctx.resolveConfirmation === undefined ? {} : { resolveConfirmation: ctx.resolveConfirmation }),
|
|
45
|
+
i18n: ctx.i18n,
|
|
42
46
|
log: ctx.log,
|
|
43
47
|
});
|
|
44
48
|
},
|
package/messages.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Google Chat channel's texts a person sees, in English: the base every
|
|
3
|
+
* instance's language files translate (`ChannelPlugin.messages`). The cards'
|
|
4
|
+
* titles and status lines, the confirm button of a plain staging; the replies
|
|
5
|
+
* and the tool labels are the core's and the plugins'.
|
|
6
|
+
*/
|
|
7
|
+
import type { Messages, Translate } from "@mercury-fw/channel-types";
|
|
8
|
+
|
|
9
|
+
export const GOOGLE_CHAT_MESSAGES: Messages = {
|
|
10
|
+
// The confirmation card (`buildConfirmCard`, `buildChosenCard`).
|
|
11
|
+
"google-chat.confirm.title": "Confirmation needed",
|
|
12
|
+
"google-chat.confirm.button": "Confirm",
|
|
13
|
+
"google-chat.confirm.chosen": "Chosen: {label}",
|
|
14
|
+
"google-chat.confirm.taken": "Choice recorded.",
|
|
15
|
+
// A tool call's or a reasoning burst's card (`buildToolCallCard`).
|
|
16
|
+
"google-chat.status.loading": "In progress…",
|
|
17
|
+
"google-chat.status.success": "Done.",
|
|
18
|
+
"google-chat.status.failed": "Failed.",
|
|
19
|
+
"google-chat.status.pending": "Waiting for confirmation.",
|
|
20
|
+
"google-chat.thinking": "Thinking…",
|
|
21
|
+
// The card sent as soon as a message arrives (`buildReceivingCard`).
|
|
22
|
+
"google-chat.receiving.title": "Status",
|
|
23
|
+
"google-chat.receiving.text": "Message received…",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** These texts in English, for a provider built without the core's (`ChannelI18n`); any other key comes back as itself. */
|
|
27
|
+
export const englishChat: Translate = (key, params) =>
|
|
28
|
+
(GOOGLE_CHAT_MESSAGES[key] ?? key).replace(/\{(\w+)\}/g, (whole, name: string) =>
|
|
29
|
+
params !== undefined && Object.hasOwn(params, name) ? String(params[name]) : whole,
|
|
30
|
+
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/channel-google-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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.
|
|
34
|
+
"@mercury-fw/channel-types": ">=0.54.0 <1.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@mercury-fw/channel-types": "0.
|
|
37
|
+
"@mercury-fw/channel-types": "0.54.0",
|
|
38
38
|
"@mercury-fw/typescript-config": "*",
|
|
39
39
|
"@types/bun": "^1.4.2",
|
|
40
40
|
"typescript": "^6.0.3"
|