@mercury-fw/channel-google-chat 0.9.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.
- package/CHANGELOG.md +31 -0
- package/README.md +5 -2
- package/dist/google-chat-provider.d.ts +26 -2
- package/google-chat-provider.ts +266 -31
- package/messages.ts +4 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @mercury-fw/channel-google-chat
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e3eb49d: Stop an answer from Google Chat.
|
|
8
|
+
|
|
9
|
+
- Every card still in progress (the receiving card, a reasoning card, a tool's card) has a Stop button, in a section of its own so it's visible without expanding the card; the card's final state has none.
|
|
10
|
+
- `/stop` does the same: the registered slash command, or a message reading exactly `/stop`. With nothing running, the reply says there's nothing to stop.
|
|
11
|
+
- A stop aborts the person's running turn and discards the messages they queued behind it. No partial answer is sent; the card in progress says "Stopped." (or a short message does, when no card was in progress). A command a tool already ran stays done.
|
|
12
|
+
- Only the person who started the answer can stop it: in a shared space, someone else's stop doesn't touch it.
|
|
13
|
+
- New texts: `google-chat.status.stopped`, `google-chat.stop.button`, `google-chat.stop.nothing`.
|
|
14
|
+
|
|
15
|
+
Needs the framework at 0.58 or later; an older core stops the model too, but sends the half answer it had. To have Chat suggest `/stop`, and to let it reach the app in a space without a mention, add it in the Chat app's configuration (Cloud Console, Google Chat API → Configuration → Commands): type Slash command, name `/stop`, any free command id.
|
|
16
|
+
|
|
17
|
+
## 0.10.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- 1126106: Every turn tells the model when and where the person wrote it.
|
|
22
|
+
|
|
23
|
+
- Each message reaches the model after a line with the weekday, date, time and time zone it was written in (`[Wednesday 2026-10-21 14:32 Europe/Rome]`), and the system prompt says what that line is. The conversation's archive and what a UI shows of it keep the message as written.
|
|
24
|
+
- A resumed conversation's messages come back stamped from the archive's timestamps.
|
|
25
|
+
- The Layer 1 summary keeps when things were said; the episodic summary quotes a date only from those lines, never guessing one. The semantic-fact extractor and the capture card's preview leave the line out.
|
|
26
|
+
- The time zone, always an IANA name (`Europe/Rome`), is the one the person saved, else the one their client is in when the channel passes it on, else the new `i18n.timeZone` in `mercury.config.ts`, else the machine's. A `timeZone` that isn't a zone's name stops the start.
|
|
27
|
+
- The person saves theirs by telling the assistant (the new `set_time_zone` tool), or from a channel's settings.
|
|
28
|
+
- Channel contract: `InboundTurn.timeZoneHint`, and `ctx.settings.setTimeZone` for a channel with a settings area; `reads.me` also returns the saved `timeZone`. Both are optional additions: the contract version stays 6.
|
|
29
|
+
- HTTP channel: the `X-Time-Zone` header on `POST /turn` (allowed by CORS), `timeZone` in `GET /me`, and `PATCH /me` with `{ timeZone }` to save it. Needs the framework at 0.56 or later; an older core ignores the header and offers no `PATCH /me`.
|
|
30
|
+
- Google Chat channel: the client's time zone (`common.timeZone`), when the event carries it, on messages and on the turn a click resumes.
|
|
31
|
+
|
|
32
|
+
To set a default for an app's people, add `timeZone: "Europe/Rome"` (their zone) to `i18n` in `mercury.config.ts`; without it the machine's zone is used, often UTC inside a container.
|
|
33
|
+
|
|
3
34
|
## 0.9.0
|
|
4
35
|
|
|
5
36
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -26,14 +26,16 @@ 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.
|
|
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. The client's time zone reaches the model from the framework at 0.56 on; an older core ignores it. Stopping an answer needs the framework at 0.58 or later: an older core stops the model as well, but then sends the half answer it had.
|
|
30
30
|
|
|
31
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.
|
|
32
32
|
|
|
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.
|
|
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. The time zone of their Chat client, when the event carries it (`common.timeZone`), goes to the core the same way: the model reads each message with the time there, unless the person saved a zone of their own.
|
|
34
34
|
|
|
35
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.
|
|
36
36
|
|
|
37
|
+
While it works, every card still in progress (the one that says the message arrived, a reasoning card, a tool's card) has a Stop button, and sending `/stop` does the same. A stop ends the answer the person was waiting for and throws away the messages they had queued behind it: nothing of the half answer is sent, the card in progress says it stopped (or a short message does, when no card was in progress), and the conversation keeps their message with a note that the answer was stopped, so whatever they write next still has its context. A command a tool already ran stays done, since the stop cuts the model, not the command. Only the person who started the answer can stop it: in a shared space someone else's Stop or `/stop` finds nothing of theirs running, and `/stop` with nothing running says so. The button works as soon as the channel is installed, while `/stop` typed as a message works too, the registered slash command (step 7, item 8) only adds it to the commands Chat suggests, and lets it reach the app in a space without mentioning it.
|
|
38
|
+
|
|
37
39
|
## Table of contents
|
|
38
40
|
|
|
39
41
|
- [Setting up the Chat app](#setting-up-the-chat-app)
|
|
@@ -145,6 +147,7 @@ Open the Chat API's configuration page for the project, `https://console.develop
|
|
|
145
147
|
5. **Connection settings**: pick **Cloud Pub/Sub** and paste the topic's full name, `projects/<PROJECT_ID>/topics/<TOPIC>`.
|
|
146
148
|
6. **Visibility**: check **Make this Google Chat app available to specific people and groups in <your domain>** and enter who can use it (people or a group).
|
|
147
149
|
7. **Logs**: check **Log errors to Logging**, so a delivery error from Chat shows up in the project's logs.
|
|
150
|
+
8. **Commands**: add a command of type **Slash command**, named `/stop`, with any free **Command ID** (`1` on a new app) and a description such as "Stop the answer in progress". The channel recognises it by its name, so the id is up to you.
|
|
148
151
|
|
|
149
152
|
Click **Save**. The field names here come from Google's own Pub/Sub quickstart; Console moves things around now and then, so trust the meaning over the exact position.
|
|
150
153
|
|
|
@@ -53,6 +53,8 @@ export type ParsedMessageEvent = {
|
|
|
53
53
|
isDirectMessage: boolean;
|
|
54
54
|
/** The language of the sender's Chat client (`common.userLocale`), when the event carries it: the turn's language hint. */
|
|
55
55
|
locale?: string;
|
|
56
|
+
/** The time zone of the sender's Chat client (`common.timeZone.id`), when the event carries it: the turn's time zone hint. */
|
|
57
|
+
timeZone?: string;
|
|
56
58
|
};
|
|
57
59
|
export type ParsedCardClickEvent = {
|
|
58
60
|
kind: "card-click";
|
|
@@ -66,12 +68,34 @@ export type ParsedCardClickEvent = {
|
|
|
66
68
|
isDirectMessage: boolean;
|
|
67
69
|
/** As on a message (see `ParsedMessageEvent.locale`). */
|
|
68
70
|
locale?: string;
|
|
71
|
+
/** As on a message (see `ParsedMessageEvent.timeZone`). */
|
|
72
|
+
timeZone?: string;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The person asked to stop their running turn: `/stop` (`source: "command"`,
|
|
76
|
+
* the registered slash command or a message reading exactly that), or a click
|
|
77
|
+
* on a card's Stop button (`source: "button"`).
|
|
78
|
+
*/
|
|
79
|
+
export type ParsedStopEvent = {
|
|
80
|
+
kind: "stop";
|
|
81
|
+
source: "command" | "button";
|
|
82
|
+
space: string;
|
|
83
|
+
sender: string;
|
|
84
|
+
senderEmail: string | undefined;
|
|
85
|
+
/** As on a message (see `ParsedMessageEvent.isDirectMessage`). */
|
|
86
|
+
isDirectMessage: boolean;
|
|
87
|
+
/** As on a message (see `ParsedMessageEvent.locale`): for the reply when there's nothing to stop. */
|
|
88
|
+
locale?: string;
|
|
89
|
+
/** For a click, the turn whose card carried the button: only that turn is stopped. */
|
|
90
|
+
turn?: string;
|
|
69
91
|
};
|
|
70
92
|
/** Parses one decoded Pub/Sub event, or `null` if it isn't a kind this provider acts on. */
|
|
71
|
-
export declare function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardClickEvent | null;
|
|
93
|
+
export declare function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardClickEvent | ParsedStopEvent | null;
|
|
72
94
|
export type CardClickHandler = (params: Record<string, string>, space: string, sender: string, senderEmail?: string, isDirectMessage?: boolean, messageName?: string,
|
|
73
95
|
/** The clicking person's client language (`ParsedCardClickEvent.locale`). */
|
|
74
|
-
locale?: string
|
|
96
|
+
locale?: string,
|
|
97
|
+
/** The clicking person's client time zone (`ParsedCardClickEvent.timeZone`). */
|
|
98
|
+
timeZone?: string) => Promise<void>;
|
|
75
99
|
export type GoogleChatProviderDeps = {
|
|
76
100
|
credentials: ServiceAccountCredentials;
|
|
77
101
|
subscription: string;
|
package/google-chat-provider.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type ServiceAccountCredentials,
|
|
29
29
|
type TokenSource,
|
|
30
30
|
type ChatCard,
|
|
31
|
+
type ChatCardSection,
|
|
31
32
|
} from "./google-chat-app-client.ts";
|
|
32
33
|
import { openSubscription, type PubSubSubscription, type StreamMessage } from "./google-chat-pubsub-stream.ts";
|
|
33
34
|
import { splitForSendLimit } from "./google-chat-message-buffer.ts";
|
|
@@ -113,11 +114,35 @@ function buildLoginCard(login: LoginRequest, t: Translate): ChatCard {
|
|
|
113
114
|
};
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
/** Where a status card stands: in progress, ended as a tool ends, or stopped by the person. */
|
|
118
|
+
type CardStatus = "loading" | ToolOutcome | "stopped";
|
|
119
|
+
|
|
116
120
|
/** A card's status line, in the person's language. */
|
|
117
|
-
function statusLine(status:
|
|
121
|
+
function statusLine(status: CardStatus, t: Translate): string {
|
|
118
122
|
return t(`google-chat.status.${status}`);
|
|
119
123
|
}
|
|
120
124
|
|
|
125
|
+
/**
|
|
126
|
+
* The section that carries the Stop button on a card in progress: a section of
|
|
127
|
+
* its own, outside the collapsible one, so it's visible without expanding
|
|
128
|
+
* anything. A click comes back as `CARD_CLICKED` with the `stop` parameter
|
|
129
|
+
* set to `turn`, the id of the turn the card belongs to (`parseChatEvent`), so
|
|
130
|
+
* a button left on an older card never stops a newer turn. A card's final
|
|
131
|
+
* patch rebuilds it without this section, so the button goes away with the
|
|
132
|
+
* turn.
|
|
133
|
+
*/
|
|
134
|
+
function stopSection(t: Translate, turn: string): ChatCardSection {
|
|
135
|
+
return {
|
|
136
|
+
widgets: [
|
|
137
|
+
{
|
|
138
|
+
buttonList: {
|
|
139
|
+
buttons: [{ text: t("google-chat.stop.button"), onClick: { action: { function: "stop", parameters: [{ key: "stop", value: turn }] } } }],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
121
146
|
/**
|
|
122
147
|
* A tool-call/reasoning status card: `label` is a section `header`
|
|
123
148
|
* (Google Chat's native accordion title, always visible regardless of
|
|
@@ -146,7 +171,7 @@ function statusLine(status: "loading" | ToolOutcome, t: Translate): string {
|
|
|
146
171
|
* nothing patches the message again, so the native toggle can never be
|
|
147
172
|
* reset out from under the user again either.
|
|
148
173
|
*/
|
|
149
|
-
function buildToolCallCard(label: string, detail: string, status:
|
|
174
|
+
function buildToolCallCard(label: string, detail: string, status: CardStatus, t: Translate, turn: string): ChatCard {
|
|
150
175
|
return {
|
|
151
176
|
sections: [
|
|
152
177
|
{
|
|
@@ -155,6 +180,7 @@ function buildToolCallCard(label: string, detail: string, status: "loading" | To
|
|
|
155
180
|
uncollapsibleWidgetsCount: 0,
|
|
156
181
|
widgets: [{ textParagraph: { text: detail } }, { textParagraph: { text: statusLine(status, t) } }],
|
|
157
182
|
},
|
|
183
|
+
...(status === "loading" ? [stopSection(t, turn)] : []),
|
|
158
184
|
],
|
|
159
185
|
};
|
|
160
186
|
}
|
|
@@ -182,10 +208,17 @@ const REASONING_TAIL_CHARS = 4000;
|
|
|
182
208
|
* in this file. If the turn's first reasoning burst arrives,
|
|
183
209
|
* `onReasoningChunk` patches this exact card into the "Thinking…"
|
|
184
210
|
* card in place, rather than sending a second message; otherwise it's
|
|
185
|
-
*
|
|
211
|
+
* left as the last status before the turn's real answer, patched only to
|
|
212
|
+
* take its Stop button away, or to say the person stopped the turn.
|
|
186
213
|
*/
|
|
187
|
-
function buildReceivingCard(t: Translate): ChatCard {
|
|
188
|
-
|
|
214
|
+
function buildReceivingCard(t: Translate, turn: string, state: "loading" | "answered" | "stopped" = "loading"): ChatCard {
|
|
215
|
+
const text = state === "stopped" ? statusLine("stopped", t) : t("google-chat.receiving.text");
|
|
216
|
+
return {
|
|
217
|
+
sections: [
|
|
218
|
+
{ header: t("google-chat.receiving.title"), widgets: [{ textParagraph: { text } }] },
|
|
219
|
+
...(state === "loading" ? [stopSection(t, turn)] : []),
|
|
220
|
+
],
|
|
221
|
+
};
|
|
189
222
|
}
|
|
190
223
|
|
|
191
224
|
/**
|
|
@@ -220,14 +253,18 @@ type RawChatEvent = {
|
|
|
220
253
|
message?: {
|
|
221
254
|
name?: string;
|
|
222
255
|
text?: string;
|
|
256
|
+
/** The text without the leading @mention of the app, in a space. */
|
|
257
|
+
argumentText?: string;
|
|
223
258
|
space?: { name?: string; type?: string };
|
|
224
259
|
sender?: RawChatSender;
|
|
260
|
+
/** A slash command the message invoked shows up here, with its name. */
|
|
261
|
+
annotations?: Array<{ type?: string; slashCommand?: { commandName?: string } }>;
|
|
225
262
|
};
|
|
226
263
|
action?: { actionMethodName?: string; parameters?: Array<{ key?: string; value?: string }> };
|
|
227
264
|
space?: { name?: string; type?: string };
|
|
228
265
|
user?: RawChatSender;
|
|
229
|
-
/** What the person's Chat client says about them
|
|
230
|
-
common?: { userLocale?: string };
|
|
266
|
+
/** What the person's Chat client says about them, when the event carries it: `userLocale` is their language (`en-US`), `timeZone.id` the zone they're in (`Europe/Rome`). */
|
|
267
|
+
common?: { userLocale?: string; timeZone?: { id?: string; offset?: number } };
|
|
231
268
|
};
|
|
232
269
|
|
|
233
270
|
export type ParsedMessageEvent = {
|
|
@@ -249,6 +286,8 @@ export type ParsedMessageEvent = {
|
|
|
249
286
|
isDirectMessage: boolean;
|
|
250
287
|
/** The language of the sender's Chat client (`common.userLocale`), when the event carries it: the turn's language hint. */
|
|
251
288
|
locale?: string;
|
|
289
|
+
/** The time zone of the sender's Chat client (`common.timeZone.id`), when the event carries it: the turn's time zone hint. */
|
|
290
|
+
timeZone?: string;
|
|
252
291
|
};
|
|
253
292
|
|
|
254
293
|
export type ParsedCardClickEvent = {
|
|
@@ -263,28 +302,77 @@ export type ParsedCardClickEvent = {
|
|
|
263
302
|
isDirectMessage: boolean;
|
|
264
303
|
/** As on a message (see `ParsedMessageEvent.locale`). */
|
|
265
304
|
locale?: string;
|
|
305
|
+
/** As on a message (see `ParsedMessageEvent.timeZone`). */
|
|
306
|
+
timeZone?: string;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* The person asked to stop their running turn: `/stop` (`source: "command"`,
|
|
311
|
+
* the registered slash command or a message reading exactly that), or a click
|
|
312
|
+
* on a card's Stop button (`source: "button"`).
|
|
313
|
+
*/
|
|
314
|
+
export type ParsedStopEvent = {
|
|
315
|
+
kind: "stop";
|
|
316
|
+
source: "command" | "button";
|
|
317
|
+
space: string;
|
|
318
|
+
sender: string;
|
|
319
|
+
senderEmail: string | undefined;
|
|
320
|
+
/** As on a message (see `ParsedMessageEvent.isDirectMessage`). */
|
|
321
|
+
isDirectMessage: boolean;
|
|
322
|
+
/** As on a message (see `ParsedMessageEvent.locale`): for the reply when there's nothing to stop. */
|
|
323
|
+
locale?: string;
|
|
324
|
+
/** For a click, the turn whose card carried the button: only that turn is stopped. */
|
|
325
|
+
turn?: string;
|
|
266
326
|
};
|
|
267
327
|
|
|
328
|
+
/** The command that stops the session's running turn. */
|
|
329
|
+
const STOP_COMMAND = "/stop";
|
|
330
|
+
|
|
331
|
+
/** Whether a message asks to stop: it invoked the `/stop` slash command, or its text (after any @mention) is exactly `/stop`. */
|
|
332
|
+
function isStopMessage(m: NonNullable<RawChatEvent["message"]>): boolean {
|
|
333
|
+
if (m.annotations?.some((a) => a.type === "SLASH_COMMAND" && a.slashCommand?.commandName === STOP_COMMAND)) return true;
|
|
334
|
+
const text = typeof m.argumentText === "string" ? m.argumentText : m.text;
|
|
335
|
+
return text?.trim() === STOP_COMMAND;
|
|
336
|
+
}
|
|
337
|
+
|
|
268
338
|
/** The language the event says the person's client is in, when it says. */
|
|
269
339
|
function localeOf(event: RawChatEvent | null | undefined): string | undefined {
|
|
270
340
|
const locale = event?.common?.userLocale;
|
|
271
341
|
return typeof locale === "string" && locale.trim() !== "" ? locale.trim() : undefined;
|
|
272
342
|
}
|
|
273
343
|
|
|
344
|
+
/** The time zone the event says the person's client is in, when it says. */
|
|
345
|
+
function timeZoneOf(event: RawChatEvent | null | undefined): string | undefined {
|
|
346
|
+
const id = event?.common?.timeZone?.id;
|
|
347
|
+
return typeof id === "string" && id.trim() !== "" ? id.trim() : undefined;
|
|
348
|
+
}
|
|
349
|
+
|
|
274
350
|
/** The email Google vouches for on `sender`: a person's only (`HUMAN`), never a bot's or an unknown kind's. */
|
|
275
351
|
function emailOf(sender: RawChatSender | undefined): string | undefined {
|
|
276
352
|
return typeof sender?.email === "string" && sender.email !== "" && sender.type === "HUMAN" ? sender.email : undefined;
|
|
277
353
|
}
|
|
278
354
|
|
|
279
355
|
/** Parses one decoded Pub/Sub event, or `null` if it isn't a kind this provider acts on. */
|
|
280
|
-
export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardClickEvent | null {
|
|
356
|
+
export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardClickEvent | ParsedStopEvent | null {
|
|
281
357
|
const event = raw as RawChatEvent;
|
|
282
358
|
const locale = localeOf(event);
|
|
359
|
+
const timeZone = timeZoneOf(event);
|
|
283
360
|
if (event?.type === "MESSAGE") {
|
|
284
361
|
const m = event.message;
|
|
285
362
|
if (!m || typeof m.name !== "string" || typeof m.text !== "string") return null;
|
|
286
363
|
if (typeof m.space?.name !== "string") return null;
|
|
287
364
|
if (typeof m.sender?.name !== "string") return null;
|
|
365
|
+
if (isStopMessage(m)) {
|
|
366
|
+
return {
|
|
367
|
+
kind: "stop",
|
|
368
|
+
source: "command",
|
|
369
|
+
space: m.space.name,
|
|
370
|
+
sender: m.sender.name,
|
|
371
|
+
senderEmail: emailOf(m.sender),
|
|
372
|
+
isDirectMessage: m.space.type === "DM",
|
|
373
|
+
...(locale === undefined ? {} : { locale }),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
288
376
|
return {
|
|
289
377
|
kind: "message",
|
|
290
378
|
text: m.text,
|
|
@@ -295,6 +383,7 @@ export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardCli
|
|
|
295
383
|
senderEmail: emailOf(m.sender),
|
|
296
384
|
isDirectMessage: m.space.type === "DM",
|
|
297
385
|
...(locale === undefined ? {} : { locale }),
|
|
386
|
+
...(timeZone === undefined ? {} : { timeZone }),
|
|
298
387
|
};
|
|
299
388
|
}
|
|
300
389
|
if (event?.type === "CARD_CLICKED") {
|
|
@@ -306,6 +395,18 @@ export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardCli
|
|
|
306
395
|
parameters[p.key] = p.value;
|
|
307
396
|
}
|
|
308
397
|
}
|
|
398
|
+
if (parameters.stop !== undefined) {
|
|
399
|
+
return {
|
|
400
|
+
kind: "stop",
|
|
401
|
+
source: "button",
|
|
402
|
+
turn: parameters.stop,
|
|
403
|
+
space: event.space.name,
|
|
404
|
+
sender: event.user.name,
|
|
405
|
+
senderEmail: emailOf(event.user),
|
|
406
|
+
isDirectMessage: event.space.type === "DM",
|
|
407
|
+
...(locale === undefined ? {} : { locale }),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
309
410
|
return {
|
|
310
411
|
kind: "card-click",
|
|
311
412
|
...(typeof event.message?.name === "string" ? { messageName: event.message.name } : {}),
|
|
@@ -315,6 +416,7 @@ export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardCli
|
|
|
315
416
|
parameters,
|
|
316
417
|
isDirectMessage: event.space.type === "DM",
|
|
317
418
|
...(locale === undefined ? {} : { locale }),
|
|
419
|
+
...(timeZone === undefined ? {} : { timeZone }),
|
|
318
420
|
};
|
|
319
421
|
}
|
|
320
422
|
return null;
|
|
@@ -329,6 +431,8 @@ export type CardClickHandler = (
|
|
|
329
431
|
messageName?: string,
|
|
330
432
|
/** The clicking person's client language (`ParsedCardClickEvent.locale`). */
|
|
331
433
|
locale?: string,
|
|
434
|
+
/** The clicking person's client time zone (`ParsedCardClickEvent.timeZone`). */
|
|
435
|
+
timeZone?: string,
|
|
332
436
|
) => Promise<void>;
|
|
333
437
|
|
|
334
438
|
export type GoogleChatProviderDeps = {
|
|
@@ -371,6 +475,15 @@ export type GoogleChatProviderDeps = {
|
|
|
371
475
|
log?: (msg: string) => void;
|
|
372
476
|
};
|
|
373
477
|
|
|
478
|
+
/**
|
|
479
|
+
* A turn's sink, plus what the provider does once the turn has let go:
|
|
480
|
+
* after a stop, `settle` makes the cards say so (the receiving card, or a
|
|
481
|
+
* short message when no card in progress could); after a turn that `failed`
|
|
482
|
+
* (threw, so never reached `finalize`), it takes the receiving card's Stop
|
|
483
|
+
* button away; after an answer, it does nothing.
|
|
484
|
+
*/
|
|
485
|
+
type StopAwareSink = TurnSink & { settle(failed: boolean): Promise<void> };
|
|
486
|
+
|
|
374
487
|
export type GoogleChatProvider = Provider & {
|
|
375
488
|
stop(): Promise<void>;
|
|
376
489
|
};
|
|
@@ -396,12 +509,14 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
396
509
|
// concurrently. Scoped by sessionKey only (not global), so a slow turn
|
|
397
510
|
// for one user/space never blocks a different one's.
|
|
398
511
|
const busySessions = new Set<string>();
|
|
399
|
-
const queuedTurns = new Map<string, Array<() => Promise<void>>>();
|
|
512
|
+
const queuedTurns = new Map<string, Array<(signal: AbortSignal, turnId: string) => Promise<void>>>();
|
|
513
|
+
// The running turn, per session: its id (on its cards' Stop buttons) and the abort a stop fires.
|
|
514
|
+
const runningTurns = new Map<string, { id: string; controller: AbortController }>();
|
|
400
515
|
// The turn handler `start` was given, for a turn a card click resumes.
|
|
401
516
|
let activeHandleTurn: HandleTurn | undefined;
|
|
402
517
|
|
|
403
|
-
/** Runs `turn` for `sessionKey` now, or after the turns already queued for it. */
|
|
404
|
-
async function runInSession(sessionKey: string, turn: () => Promise<void>): Promise<void> {
|
|
518
|
+
/** Runs `turn` for `sessionKey` now, or after the turns already queued for it, with a signal `stopSession` aborts. */
|
|
519
|
+
async function runInSession(sessionKey: string, turn: (signal: AbortSignal, turnId: string) => Promise<void>): Promise<void> {
|
|
405
520
|
if (busySessions.has(sessionKey)) {
|
|
406
521
|
const queue = queuedTurns.get(sessionKey) ?? [];
|
|
407
522
|
queue.push(turn);
|
|
@@ -409,9 +524,13 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
409
524
|
return;
|
|
410
525
|
}
|
|
411
526
|
busySessions.add(sessionKey);
|
|
527
|
+
const controller = new AbortController();
|
|
528
|
+
const id = crypto.randomUUID();
|
|
529
|
+
runningTurns.set(sessionKey, { id, controller });
|
|
412
530
|
try {
|
|
413
|
-
await turn();
|
|
531
|
+
await turn(controller.signal, id);
|
|
414
532
|
} finally {
|
|
533
|
+
runningTurns.delete(sessionKey);
|
|
415
534
|
busySessions.delete(sessionKey);
|
|
416
535
|
const queue = queuedTurns.get(sessionKey);
|
|
417
536
|
const next = queue?.shift();
|
|
@@ -422,6 +541,40 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
422
541
|
}
|
|
423
542
|
}
|
|
424
543
|
|
|
544
|
+
/**
|
|
545
|
+
* Stops `sessionKey`'s running turn and discards every turn queued behind
|
|
546
|
+
* it: they haven't sent anything yet, so they just never run. With `turnId`
|
|
547
|
+
* (a button's), only when that's the turn running: a click on an older
|
|
548
|
+
* card stops nothing. Returns whether there was a turn to stop.
|
|
549
|
+
*/
|
|
550
|
+
function stopSession(sessionKey: string, turnId?: string): boolean {
|
|
551
|
+
const running = runningTurns.get(sessionKey);
|
|
552
|
+
if (running === undefined || running.controller.signal.aborted) return false;
|
|
553
|
+
if (turnId !== undefined && turnId !== running.id) return false;
|
|
554
|
+
queuedTurns.delete(sessionKey);
|
|
555
|
+
running.controller.abort();
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Runs one turn on `handleTurn` with the session's stop signal, then
|
|
561
|
+
* settles its cards once it has let go (`StopAwareSink.settle`).
|
|
562
|
+
*/
|
|
563
|
+
async function runTurn(
|
|
564
|
+
handleTurn: HandleTurn,
|
|
565
|
+
turn: Parameters<HandleTurn>[0],
|
|
566
|
+
sink: StopAwareSink,
|
|
567
|
+
signal: AbortSignal,
|
|
568
|
+
): Promise<void> {
|
|
569
|
+
try {
|
|
570
|
+
await handleTurn({ ...turn, abortSignal: signal }, sink);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
await sink.settle(true);
|
|
573
|
+
throw err;
|
|
574
|
+
}
|
|
575
|
+
await sink.settle(false);
|
|
576
|
+
}
|
|
577
|
+
|
|
425
578
|
/** Resumes the session's turn after a confirmation whose result went back to the model (`ConfirmReply.resume`). */
|
|
426
579
|
async function resumeTurn(
|
|
427
580
|
handleTurn: HandleTurn,
|
|
@@ -429,11 +582,13 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
429
582
|
principal: Principal,
|
|
430
583
|
isDirectMessage: boolean,
|
|
431
584
|
locale: string | undefined,
|
|
585
|
+
timeZone: string | undefined,
|
|
432
586
|
): Promise<void> {
|
|
433
587
|
const sessionKey = deriveSessionKey(space, principal.id);
|
|
434
588
|
const t = await textsFor(principal, locale);
|
|
435
|
-
return runInSession(sessionKey, () =>
|
|
436
|
-
|
|
589
|
+
return runInSession(sessionKey, (signal, turnId) =>
|
|
590
|
+
runTurn(
|
|
591
|
+
handleTurn,
|
|
437
592
|
{
|
|
438
593
|
channel: "google-chat",
|
|
439
594
|
multiUser: !isDirectMessage,
|
|
@@ -443,8 +598,10 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
443
598
|
logPrefix: `[chat:${space}:${principal.id}] `,
|
|
444
599
|
resumeAfterConfirm: true,
|
|
445
600
|
...(locale === undefined ? {} : { languageHint: locale }),
|
|
601
|
+
...(timeZone === undefined ? {} : { timeZoneHint: timeZone }),
|
|
446
602
|
},
|
|
447
|
-
createSink(space, { user: principal.id, isDirectMessage }, t),
|
|
603
|
+
createSink(space, { user: principal.id, isDirectMessage }, t, signal, turnId),
|
|
604
|
+
signal,
|
|
448
605
|
),
|
|
449
606
|
);
|
|
450
607
|
}
|
|
@@ -460,7 +617,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
460
617
|
*/
|
|
461
618
|
const onCardClick: CardClickHandler =
|
|
462
619
|
deps.onCardClick ??
|
|
463
|
-
(async (params, space, sender, senderEmail, isDirectMessage = false, messageName, locale) => {
|
|
620
|
+
(async (params, space, sender, senderEmail, isDirectMessage = false, messageName, locale, timeZone) => {
|
|
464
621
|
const token = params.token;
|
|
465
622
|
if (!token) {
|
|
466
623
|
log(`[chat] card click with no token parameter`);
|
|
@@ -495,7 +652,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
495
652
|
reply = await deps.confirm(token, sessionKey, principal, locale);
|
|
496
653
|
}
|
|
497
654
|
if (reply?.resume && activeHandleTurn) {
|
|
498
|
-
await resumeTurn(activeHandleTurn, space, principal, isDirectMessage, locale);
|
|
655
|
+
await resumeTurn(activeHandleTurn, space, principal, isDirectMessage, locale, timeZone);
|
|
499
656
|
return;
|
|
500
657
|
}
|
|
501
658
|
if (reply !== null) {
|
|
@@ -510,9 +667,17 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
510
667
|
* Per-turn output sink — same responsibilities as the retired `ChatStreamer`,
|
|
511
668
|
* rebuilt against the new client. `viewer` is the person the turn is for;
|
|
512
669
|
* outside a direct message, what only they may see is sent to them alone.
|
|
513
|
-
* `t` gives the cards' texts in their language.
|
|
670
|
+
* `t` gives the cards' texts in their language. `signal` is the turn's
|
|
671
|
+
* stop: once it aborted, a reasoning block cut short ends as "Stopped"
|
|
672
|
+
* rather than "Failed". `turn` is the turn's id, on its Stop buttons.
|
|
514
673
|
*/
|
|
515
|
-
function createSink(
|
|
674
|
+
function createSink(
|
|
675
|
+
space: string,
|
|
676
|
+
viewer: { user: string; isDirectMessage: boolean },
|
|
677
|
+
t: Translate,
|
|
678
|
+
signal: AbortSignal,
|
|
679
|
+
turn: string,
|
|
680
|
+
): StopAwareSink {
|
|
516
681
|
let chain: Promise<void> = Promise.resolve();
|
|
517
682
|
|
|
518
683
|
function enqueue(fn: () => Promise<void>): void {
|
|
@@ -552,9 +717,23 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
552
717
|
|
|
553
718
|
// A tool asking again in the same turn hands back the same pending login.
|
|
554
719
|
const shownLogins = new Set<string>();
|
|
720
|
+
// Whether a card already says the person stopped the turn.
|
|
721
|
+
let stopShown = false;
|
|
722
|
+
// Set once the answer is on its way: a stop after it has nothing left to say.
|
|
723
|
+
let answered = false;
|
|
724
|
+
|
|
725
|
+
/** Patches the receiving card no reasoning claimed, to `state`: it has nothing else that would ever take its Stop button away. */
|
|
726
|
+
async function closeReceivingCard(state: "answered" | "stopped"): Promise<boolean> {
|
|
727
|
+
if (receivingCardName === undefined) return false;
|
|
728
|
+
const name = receivingCardName;
|
|
729
|
+
receivingCardName = undefined;
|
|
730
|
+
log(`[chat:${space}] [out] receiving card ${name}: ${state}`);
|
|
731
|
+
await updateCardFn(name, buildReceivingCard(t, turn, state), clientDeps);
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
555
734
|
enqueue(async () => {
|
|
556
735
|
log(`[chat:${space}] [out] receiving card`);
|
|
557
|
-
const sent = await sendCardFn(space, buildReceivingCard(t), clientDeps);
|
|
736
|
+
const sent = await sendCardFn(space, buildReceivingCard(t, turn), clientDeps);
|
|
558
737
|
sentMessageNames.add(sent.name);
|
|
559
738
|
receivingCardName = sent.name;
|
|
560
739
|
});
|
|
@@ -566,7 +745,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
566
745
|
return;
|
|
567
746
|
}
|
|
568
747
|
enqueue(async () => {
|
|
569
|
-
const card = buildToolCallCard(label, detail ?? "", "loading", t);
|
|
748
|
+
const card = buildToolCallCard(label, detail ?? "", "loading", t, turn);
|
|
570
749
|
log(`[chat:${space}] [out] tool card: ${label}`);
|
|
571
750
|
const sent = await sendCardFn(space, card, clientDeps);
|
|
572
751
|
sentMessageNames.add(sent.name);
|
|
@@ -580,7 +759,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
580
759
|
log(`[chat:${space}] onToolFinish for unknown toolCallId ${toolCallId}`);
|
|
581
760
|
return;
|
|
582
761
|
}
|
|
583
|
-
const card = buildToolCallCard(entry.label, entry.detail, outcome, t);
|
|
762
|
+
const card = buildToolCallCard(entry.label, entry.detail, outcome, t, turn);
|
|
584
763
|
log(`[chat:${space}] [out] patching ${entry.name} to "${outcome}"`);
|
|
585
764
|
const patched = await updateCardFn(entry.name, card, clientDeps);
|
|
586
765
|
log(`[chat:${space}] [out] patched ${patched.name}`);
|
|
@@ -597,7 +776,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
597
776
|
entry = { name: undefined, buffer: chunk };
|
|
598
777
|
reasoningCards.set(id, entry);
|
|
599
778
|
enqueue(async () => {
|
|
600
|
-
const card = buildToolCallCard(t("google-chat.thinking"), "", "loading", t);
|
|
779
|
+
const card = buildToolCallCard(t("google-chat.thinking"), "", "loading", t, turn);
|
|
601
780
|
if (receivingCardName !== undefined) {
|
|
602
781
|
// Replace the "Status" placeholder in place instead of
|
|
603
782
|
// sending a second message.
|
|
@@ -625,8 +804,9 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
625
804
|
if (!entry) return; // no reasoning happened for this id — no card to close
|
|
626
805
|
enqueue(async () => {
|
|
627
806
|
if (entry.name === undefined) return; // shouldn't happen given enqueue's own FIFO ordering, but stays defensive
|
|
628
|
-
const status = failed ? "
|
|
629
|
-
|
|
807
|
+
const status = !failed ? "success" : signal.aborted ? "stopped" : "failed";
|
|
808
|
+
if (status === "stopped") stopShown = true;
|
|
809
|
+
const card = buildToolCallCard(t("google-chat.thinking"), tailTruncate(entry.buffer, REASONING_TAIL_CHARS), status, t, turn);
|
|
630
810
|
log(`[chat:${space}] [out] reasoning patch (${id}): status=${status} bufferLen=${entry.buffer.length}`);
|
|
631
811
|
await updateCardFn(entry.name, card, clientDeps);
|
|
632
812
|
reasoningCards.delete(id);
|
|
@@ -663,6 +843,10 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
663
843
|
const note = info?.pendingNote;
|
|
664
844
|
const shown = note !== undefined && finalText.startsWith(note) ? finalText.slice(note.length) : finalText;
|
|
665
845
|
const trimmed = shown.trim();
|
|
846
|
+
answered = true;
|
|
847
|
+
enqueue(async () => {
|
|
848
|
+
await closeReceivingCard("answered");
|
|
849
|
+
});
|
|
666
850
|
if (trimmed.length > 0 && trimmed !== NO_REPLY) {
|
|
667
851
|
for (const message of splitForSendLimit(trimmed)) {
|
|
668
852
|
sendPlain(message);
|
|
@@ -671,6 +855,27 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
671
855
|
await chain;
|
|
672
856
|
},
|
|
673
857
|
dispose: () => {},
|
|
858
|
+
settle: async (failed) => {
|
|
859
|
+
if (answered) return;
|
|
860
|
+
if (!signal.aborted) {
|
|
861
|
+
if (!failed) return;
|
|
862
|
+
enqueue(async () => {
|
|
863
|
+
await closeReceivingCard("answered");
|
|
864
|
+
});
|
|
865
|
+
await chain;
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
enqueue(async () => {
|
|
869
|
+
if (await closeReceivingCard("stopped")) stopShown = true;
|
|
870
|
+
if (stopShown) return;
|
|
871
|
+
stopShown = true;
|
|
872
|
+
const text = statusLine("stopped", t);
|
|
873
|
+
log(`[chat:${space}] [out] ${text}`);
|
|
874
|
+
const sent = await sendMessageFn(space, text, clientDeps);
|
|
875
|
+
sentMessageNames.add(sent.name);
|
|
876
|
+
});
|
|
877
|
+
await chain;
|
|
878
|
+
},
|
|
674
879
|
};
|
|
675
880
|
}
|
|
676
881
|
|
|
@@ -692,7 +897,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
692
897
|
|
|
693
898
|
const reply = await deps.confirm(event.text, sessionKey, principal, event.locale);
|
|
694
899
|
if (reply?.resume) {
|
|
695
|
-
await resumeTurn(handleTurn, event.space, principal, event.isDirectMessage, event.locale);
|
|
900
|
+
await resumeTurn(handleTurn, event.space, principal, event.isDirectMessage, event.locale, event.timeZone);
|
|
696
901
|
return;
|
|
697
902
|
}
|
|
698
903
|
if (reply !== null) {
|
|
@@ -706,8 +911,9 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
706
911
|
// Only the actual model turn needs serializing — the confirmation above
|
|
707
912
|
// never touches SessionHistory, so it's always safe to run right
|
|
708
913
|
// away regardless of whether this session is mid-turn.
|
|
709
|
-
await runInSession(sessionKey, () =>
|
|
710
|
-
|
|
914
|
+
await runInSession(sessionKey, (signal, turnId) =>
|
|
915
|
+
runTurn(
|
|
916
|
+
handleTurn,
|
|
711
917
|
{
|
|
712
918
|
channel: "google-chat",
|
|
713
919
|
multiUser: !event.isDirectMessage,
|
|
@@ -716,12 +922,39 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
716
922
|
principal,
|
|
717
923
|
logPrefix: `[chat:${event.space}:${event.sender}] `,
|
|
718
924
|
...(event.locale === undefined ? {} : { languageHint: event.locale }),
|
|
925
|
+
...(event.timeZone === undefined ? {} : { timeZoneHint: event.timeZone }),
|
|
719
926
|
},
|
|
720
|
-
createSink(event.space, { user: event.sender, isDirectMessage: event.isDirectMessage }, t),
|
|
927
|
+
createSink(event.space, { user: event.sender, isDirectMessage: event.isDirectMessage }, t, signal, turnId),
|
|
928
|
+
signal,
|
|
721
929
|
),
|
|
722
930
|
);
|
|
723
931
|
}
|
|
724
932
|
|
|
933
|
+
/**
|
|
934
|
+
* A stop from the person: aborts their running turn in this space and
|
|
935
|
+
* discards what's queued behind it (`stopSession`), ahead of the queue it
|
|
936
|
+
* would otherwise wait in. Only their own session: in a shared space,
|
|
937
|
+
* someone else's stop finds nothing of theirs running. `/stop` with nothing
|
|
938
|
+
* to stop says so; a click on a card whose turn is over does nothing.
|
|
939
|
+
*/
|
|
940
|
+
async function processStopEvent(event: ParsedStopEvent): Promise<void> {
|
|
941
|
+
const sessionKey = deriveSessionKey(event.space, event.sender);
|
|
942
|
+
if (stopSession(sessionKey, event.turn)) {
|
|
943
|
+
log(`[chat:${event.space}:${event.sender}] [in] stop (${event.source})`);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
log(`[chat:${event.space}:${event.sender}] [in] stop (${event.source}), nothing running`);
|
|
947
|
+
if (event.source !== "command") return;
|
|
948
|
+
const principal: Principal = {
|
|
949
|
+
id: event.sender,
|
|
950
|
+
provider: "google-chat",
|
|
951
|
+
...(event.senderEmail === undefined ? {} : { claims: { email: event.senderEmail } }),
|
|
952
|
+
};
|
|
953
|
+
const t = await textsFor(principal, event.locale);
|
|
954
|
+
const sent = await sendMessageFn(event.space, t("google-chat.stop.nothing"), clientDeps);
|
|
955
|
+
sentMessageNames.add(sent.name);
|
|
956
|
+
}
|
|
957
|
+
|
|
725
958
|
/**
|
|
726
959
|
* Acks each message immediately on arrival — before `handleTurn` (or
|
|
727
960
|
* `onCardClick`) ever runs, not after it resolves. Same pattern the
|
|
@@ -738,7 +971,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
738
971
|
*/
|
|
739
972
|
async function handleMessage(message: StreamMessage, handleTurn: HandleTurn): Promise<void> {
|
|
740
973
|
message.ack();
|
|
741
|
-
let parsed: ParsedMessageEvent | ParsedCardClickEvent | null;
|
|
974
|
+
let parsed: ParsedMessageEvent | ParsedCardClickEvent | ParsedStopEvent | null;
|
|
742
975
|
try {
|
|
743
976
|
parsed = parseChatEvent(JSON.parse(message.data.toString("utf-8")));
|
|
744
977
|
} catch (err) {
|
|
@@ -749,8 +982,10 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
749
982
|
try {
|
|
750
983
|
if (parsed.kind === "message") {
|
|
751
984
|
await processMessageEvent(parsed, handleTurn);
|
|
985
|
+
} else if (parsed.kind === "stop") {
|
|
986
|
+
await processStopEvent(parsed);
|
|
752
987
|
} else {
|
|
753
|
-
await onCardClick(parsed.parameters, parsed.space, parsed.sender, parsed.senderEmail, parsed.isDirectMessage, parsed.messageName, parsed.locale);
|
|
988
|
+
await onCardClick(parsed.parameters, parsed.space, parsed.sender, parsed.senderEmail, parsed.isDirectMessage, parsed.messageName, parsed.locale, parsed.timeZone);
|
|
754
989
|
}
|
|
755
990
|
} catch (err) {
|
|
756
991
|
log(`[chat] event handling failed: ${String(err)}`);
|
package/messages.ts
CHANGED
|
@@ -17,10 +17,14 @@ export const GOOGLE_CHAT_MESSAGES: Messages = {
|
|
|
17
17
|
"google-chat.status.success": "Done.",
|
|
18
18
|
"google-chat.status.failed": "Failed.",
|
|
19
19
|
"google-chat.status.pending": "Waiting for confirmation.",
|
|
20
|
+
"google-chat.status.stopped": "Stopped.",
|
|
20
21
|
"google-chat.thinking": "Thinking…",
|
|
21
22
|
// The card sent as soon as a message arrives (`buildReceivingCard`).
|
|
22
23
|
"google-chat.receiving.title": "Status",
|
|
23
24
|
"google-chat.receiving.text": "Message received…",
|
|
25
|
+
// The Stop button on a card in progress (`stopSection`), and `/stop` with nothing running.
|
|
26
|
+
"google-chat.stop.button": "Stop",
|
|
27
|
+
"google-chat.stop.nothing": "Nothing to stop.",
|
|
24
28
|
};
|
|
25
29
|
|
|
26
30
|
/** These texts in English, for a provider built without the core's (`ChannelI18n`); any other key comes back as itself. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/channel-google-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
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.58.0",
|
|
38
38
|
"@mercury-fw/typescript-config": "*",
|
|
39
39
|
"@types/bun": "^1.4.2",
|
|
40
40
|
"typescript": "^6.0.3"
|