@copilotkit/channels-slack 0.1.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/LICENSE +21 -0
- package/README.md +353 -0
- package/dist/__tests__/assistant.test.d.ts +2 -0
- package/dist/__tests__/assistant.test.d.ts.map +1 -0
- package/dist/__tests__/assistant.test.js +153 -0
- package/dist/__tests__/auto-close-streaming.test.d.ts +2 -0
- package/dist/__tests__/auto-close-streaming.test.d.ts.map +1 -0
- package/dist/__tests__/auto-close-streaming.test.js +139 -0
- package/dist/__tests__/built-ins.test.d.ts +2 -0
- package/dist/__tests__/built-ins.test.d.ts.map +1 -0
- package/dist/__tests__/built-ins.test.js +67 -0
- package/dist/__tests__/chunked-message-stream.test.d.ts +2 -0
- package/dist/__tests__/chunked-message-stream.test.d.ts.map +1 -0
- package/dist/__tests__/chunked-message-stream.test.js +226 -0
- package/dist/__tests__/conversation-store.test.d.ts +2 -0
- package/dist/__tests__/conversation-store.test.d.ts.map +1 -0
- package/dist/__tests__/conversation-store.test.js +209 -0
- package/dist/__tests__/download-files.test.d.ts +2 -0
- package/dist/__tests__/download-files.test.d.ts.map +1 -0
- package/dist/__tests__/download-files.test.js +142 -0
- package/dist/__tests__/ephemeral.test.d.ts +2 -0
- package/dist/__tests__/ephemeral.test.d.ts.map +1 -0
- package/dist/__tests__/ephemeral.test.js +85 -0
- package/dist/__tests__/event-renderer.test.d.ts +2 -0
- package/dist/__tests__/event-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/event-renderer.test.js +521 -0
- package/dist/__tests__/markdown-to-mrkdwn.test.d.ts +2 -0
- package/dist/__tests__/markdown-to-mrkdwn.test.d.ts.map +1 -0
- package/dist/__tests__/markdown-to-mrkdwn.test.js +80 -0
- package/dist/__tests__/message-stream.test.d.ts +2 -0
- package/dist/__tests__/message-stream.test.d.ts.map +1 -0
- package/dist/__tests__/message-stream.test.js +116 -0
- package/dist/__tests__/native-renderer.test.d.ts +2 -0
- package/dist/__tests__/native-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/native-renderer.test.js +236 -0
- package/dist/__tests__/native-stream.test.d.ts +2 -0
- package/dist/__tests__/native-stream.test.d.ts.map +1 -0
- package/dist/__tests__/native-stream.test.js +235 -0
- package/dist/__tests__/reactions.test.d.ts +2 -0
- package/dist/__tests__/reactions.test.d.ts.map +1 -0
- package/dist/__tests__/reactions.test.js +193 -0
- package/dist/__tests__/slack-listener.test.d.ts +2 -0
- package/dist/__tests__/slack-listener.test.d.ts.map +1 -0
- package/dist/__tests__/slack-listener.test.js +559 -0
- package/dist/__tests__/views.test.d.ts +2 -0
- package/dist/__tests__/views.test.d.ts.map +1 -0
- package/dist/__tests__/views.test.js +143 -0
- package/dist/adapter.d.ts +188 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +859 -0
- package/dist/adapter.test.d.ts +2 -0
- package/dist/adapter.test.d.ts.map +1 -0
- package/dist/adapter.test.js +368 -0
- package/dist/assistant.d.ts +19 -0
- package/dist/assistant.d.ts.map +1 -0
- package/dist/assistant.js +124 -0
- package/dist/auto-close-streaming.d.ts +62 -0
- package/dist/auto-close-streaming.d.ts.map +1 -0
- package/dist/auto-close-streaming.js +273 -0
- package/dist/built-in-context.d.ts +22 -0
- package/dist/built-in-context.d.ts.map +1 -0
- package/dist/built-in-context.js +65 -0
- package/dist/built-in-tools.d.ts +23 -0
- package/dist/built-in-tools.d.ts.map +1 -0
- package/dist/built-in-tools.js +45 -0
- package/dist/chunked-message-stream.d.ts +67 -0
- package/dist/chunked-message-stream.d.ts.map +1 -0
- package/dist/chunked-message-stream.js +134 -0
- package/dist/codec.d.ts +18 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +18 -0
- package/dist/codec.test.d.ts +2 -0
- package/dist/codec.test.d.ts.map +1 -0
- package/dist/codec.test.js +19 -0
- package/dist/conversation-store.d.ts +92 -0
- package/dist/conversation-store.d.ts.map +1 -0
- package/dist/conversation-store.js +196 -0
- package/dist/download-files.d.ts +59 -0
- package/dist/download-files.d.ts.map +1 -0
- package/dist/download-files.js +116 -0
- package/dist/event-renderer.d.ts +98 -0
- package/dist/event-renderer.d.ts.map +1 -0
- package/dist/event-renderer.js +493 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/ingress-normalize.d.ts +75 -0
- package/dist/ingress-normalize.d.ts.map +1 -0
- package/dist/ingress-normalize.js +153 -0
- package/dist/ingress-normalize.test.d.ts +2 -0
- package/dist/ingress-normalize.test.d.ts.map +1 -0
- package/dist/ingress-normalize.test.js +187 -0
- package/dist/interaction.d.ts +30 -0
- package/dist/interaction.d.ts.map +1 -0
- package/dist/interaction.js +219 -0
- package/dist/interaction.test.d.ts +2 -0
- package/dist/interaction.test.d.ts.map +1 -0
- package/dist/interaction.test.js +137 -0
- package/dist/markdown-to-mrkdwn.d.ts +21 -0
- package/dist/markdown-to-mrkdwn.d.ts.map +1 -0
- package/dist/markdown-to-mrkdwn.js +99 -0
- package/dist/message-stream.d.ts +44 -0
- package/dist/message-stream.d.ts.map +1 -0
- package/dist/message-stream.js +70 -0
- package/dist/native-stream.d.ts +131 -0
- package/dist/native-stream.d.ts.map +1 -0
- package/dist/native-stream.js +208 -0
- package/dist/render/block-kit.d.ts +35 -0
- package/dist/render/block-kit.d.ts.map +1 -0
- package/dist/render/block-kit.js +399 -0
- package/dist/render/block-kit.test.d.ts +2 -0
- package/dist/render/block-kit.test.d.ts.map +1 -0
- package/dist/render/block-kit.test.js +289 -0
- package/dist/render/budget.d.ts +24 -0
- package/dist/render/budget.d.ts.map +1 -0
- package/dist/render/budget.js +30 -0
- package/dist/render/budget.test.d.ts +2 -0
- package/dist/render/budget.test.d.ts.map +1 -0
- package/dist/render/budget.test.js +49 -0
- package/dist/render/modal.d.ts +13 -0
- package/dist/render/modal.d.ts.map +1 -0
- package/dist/render/modal.js +108 -0
- package/dist/render/transport.d.ts +42 -0
- package/dist/render/transport.d.ts.map +1 -0
- package/dist/render/transport.js +1 -0
- package/dist/render.d.ts +23 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +20 -0
- package/dist/sanitizing-http-agent.d.ts +28 -0
- package/dist/sanitizing-http-agent.d.ts.map +1 -0
- package/dist/sanitizing-http-agent.js +51 -0
- package/dist/slack-listener.d.ts +73 -0
- package/dist/slack-listener.d.ts.map +1 -0
- package/dist/slack-listener.js +126 -0
- package/dist/slack-listener.test.d.ts +2 -0
- package/dist/slack-listener.test.d.ts.map +1 -0
- package/dist/slack-listener.test.js +115 -0
- package/dist/types.d.ts +153 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +20 -0
- package/package.json +73 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { decodeInteraction, conversationKeyOf } from "./interaction.js";
|
|
3
|
+
import { DM_SCOPE } from "./types.js";
|
|
4
|
+
describe("conversationKeyOf", () => {
|
|
5
|
+
it("joins channelId + scope with the canonical separator", () => {
|
|
6
|
+
expect(conversationKeyOf({ channelId: "C1", scope: "100.0" })).toBe("C1::100.0");
|
|
7
|
+
expect(conversationKeyOf({ channelId: "D9", scope: DM_SCOPE })).toBe("D9::dm");
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
describe("decodeInteraction", () => {
|
|
11
|
+
it("extracts the opaque action_id + tiny value from a thread block_actions", () => {
|
|
12
|
+
const evt = decodeInteraction({
|
|
13
|
+
type: "block_actions",
|
|
14
|
+
user: { id: "U1", name: "Ana" },
|
|
15
|
+
channel: { id: "C1" },
|
|
16
|
+
message: { ts: "111.1", thread_ts: "100.0" },
|
|
17
|
+
actions: [{ action_id: "ck:abc", value: '{"confirmed":true}' }],
|
|
18
|
+
});
|
|
19
|
+
expect(evt).toBeDefined();
|
|
20
|
+
expect(evt.id).toBe("ck:abc");
|
|
21
|
+
expect(evt.value).toEqual({ confirmed: true });
|
|
22
|
+
expect(evt.conversationKey).toBe("C1::100.0");
|
|
23
|
+
expect(evt.replyTarget).toEqual({ channel: "C1", threadTs: "100.0" });
|
|
24
|
+
expect(evt.user).toEqual({ id: "U1", name: "Ana" });
|
|
25
|
+
expect(evt.messageRef).toEqual({ id: "111.1", channel: "C1" });
|
|
26
|
+
});
|
|
27
|
+
it("uses DM_SCOPE and a flat replyTarget for DM channels", () => {
|
|
28
|
+
const evt = decodeInteraction({
|
|
29
|
+
type: "block_actions",
|
|
30
|
+
user: { id: "U2", username: "bob" },
|
|
31
|
+
channel: { id: "D5" },
|
|
32
|
+
message: { ts: "9.9" },
|
|
33
|
+
actions: [{ action_id: "ck:dm", value: "yes" }],
|
|
34
|
+
});
|
|
35
|
+
expect(evt.conversationKey).toBe("D5::dm");
|
|
36
|
+
expect(evt.replyTarget).toEqual({ channel: "D5", threadTs: undefined });
|
|
37
|
+
expect(evt.value).toBe("yes");
|
|
38
|
+
expect(evt.user).toEqual({ id: "U2", name: "bob" });
|
|
39
|
+
});
|
|
40
|
+
it("scopes a THREADED DM (assistant pane) by its thread ts, not DM_SCOPE", () => {
|
|
41
|
+
// Regression: an assistant-pane DM is threaded, so the ingress path keys
|
|
42
|
+
// the turn by thread ts. Forcing DM_SCOPE here stranded the HITL waiter and
|
|
43
|
+
// the run never resumed after a Create/Cancel click.
|
|
44
|
+
const evt = decodeInteraction({
|
|
45
|
+
type: "block_actions",
|
|
46
|
+
user: { id: "U3", name: "Cara" },
|
|
47
|
+
channel: { id: "D7" },
|
|
48
|
+
message: { ts: "300.1", thread_ts: "300.0" },
|
|
49
|
+
actions: [{ action_id: "ck:hitl", value: '{"confirmed":true}' }],
|
|
50
|
+
});
|
|
51
|
+
expect(evt.conversationKey).toBe("D7::300.0");
|
|
52
|
+
// Replies should go back into the assistant thread.
|
|
53
|
+
expect(evt.replyTarget).toEqual({ channel: "D7", threadTs: "300.0" });
|
|
54
|
+
expect(evt.value).toEqual({ confirmed: true });
|
|
55
|
+
});
|
|
56
|
+
it("falls back to container fields when message/channel are absent", () => {
|
|
57
|
+
const evt = decodeInteraction({
|
|
58
|
+
type: "block_actions",
|
|
59
|
+
container: { channel_id: "C3", thread_ts: "200.0" },
|
|
60
|
+
actions: [{ action_id: "ck:c", selected_option: { value: "opt-1" } }],
|
|
61
|
+
});
|
|
62
|
+
expect(evt.conversationKey).toBe("C3::200.0");
|
|
63
|
+
expect(evt.value).toBe("opt-1");
|
|
64
|
+
expect(evt.user).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
it("decodes a multi_static_select's selected_options into a string[] value", () => {
|
|
67
|
+
const evt = decodeInteraction({
|
|
68
|
+
type: "block_actions",
|
|
69
|
+
container: { channel_id: "C3", thread_ts: "200.0" },
|
|
70
|
+
actions: [
|
|
71
|
+
{
|
|
72
|
+
action_id: "ck:ms",
|
|
73
|
+
selected_options: [{ value: "core" }, { value: "infra" }],
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
expect(evt.value).toEqual(["core", "infra"]);
|
|
78
|
+
});
|
|
79
|
+
it("returns undefined for non-block_actions or missing action_id", () => {
|
|
80
|
+
expect(decodeInteraction({ type: "view_submission" })).toBeUndefined();
|
|
81
|
+
expect(decodeInteraction({ type: "block_actions", actions: [] })).toBeUndefined();
|
|
82
|
+
expect(decodeInteraction({ type: "block_actions", actions: [{ value: "x" }] })).toBeUndefined();
|
|
83
|
+
});
|
|
84
|
+
it("returns undefined when no channel can be resolved", () => {
|
|
85
|
+
expect(decodeInteraction({
|
|
86
|
+
type: "block_actions",
|
|
87
|
+
actions: [{ action_id: "ck:x" }],
|
|
88
|
+
})).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
it("carries a stable eventId from channel + message ts + action_ts (inbound dedup)", () => {
|
|
91
|
+
const payload = {
|
|
92
|
+
type: "block_actions",
|
|
93
|
+
trigger_id: "trig-123",
|
|
94
|
+
channel: { id: "C1" },
|
|
95
|
+
message: { ts: "111.1", thread_ts: "100.0" },
|
|
96
|
+
actions: [
|
|
97
|
+
{ action_id: "ck:abc", value: "yes", action_ts: "1700000000.5" },
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
const evt = decodeInteraction(payload);
|
|
101
|
+
expect(evt.eventId).toBe("C1:111.1:1700000000.5");
|
|
102
|
+
// Stable: decoding the same payload yields the same eventId.
|
|
103
|
+
expect(decodeInteraction(payload).eventId).toBe(evt.eventId);
|
|
104
|
+
});
|
|
105
|
+
it("falls back to trigger_id for eventId when message/action ts are absent", () => {
|
|
106
|
+
const evt = decodeInteraction({
|
|
107
|
+
type: "block_actions",
|
|
108
|
+
trigger_id: "trig-xyz",
|
|
109
|
+
container: { channel_id: "C3" },
|
|
110
|
+
actions: [{ action_id: "ck:c", value: "x" }],
|
|
111
|
+
});
|
|
112
|
+
expect(evt.eventId).toBe("trig-xyz");
|
|
113
|
+
});
|
|
114
|
+
it("does NOT require a resume field (opaque id only)", () => {
|
|
115
|
+
const evt = decodeInteraction({
|
|
116
|
+
type: "block_actions",
|
|
117
|
+
channel: { id: "C2" },
|
|
118
|
+
message: { ts: "9.9" },
|
|
119
|
+
actions: [{ action_id: "ck:x" }],
|
|
120
|
+
});
|
|
121
|
+
expect(evt.id).toBe("ck:x");
|
|
122
|
+
// value is undefined when the button carried none — fine; durability rides
|
|
123
|
+
// on the ActionStore, not the payload.
|
|
124
|
+
expect(evt.value).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
it("carries trigger_id from a block_actions payload", () => {
|
|
127
|
+
const evt = decodeInteraction({
|
|
128
|
+
type: "block_actions",
|
|
129
|
+
trigger_id: "T123.456",
|
|
130
|
+
user: { id: "U1" },
|
|
131
|
+
channel: { id: "C1" },
|
|
132
|
+
message: { ts: "1.0" },
|
|
133
|
+
actions: [{ action_id: "ck:x", value: "v" }],
|
|
134
|
+
});
|
|
135
|
+
expect(evt.triggerId).toBe("T123.456");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate the agent's standard Markdown into Slack's `mrkdwn` flavour.
|
|
3
|
+
*
|
|
4
|
+
* Markdown → mrkdwn
|
|
5
|
+
* **bold** → *bold*
|
|
6
|
+
* __bold__ → *bold*
|
|
7
|
+
* *italic* → _italic_
|
|
8
|
+
* _italic_ → _italic_
|
|
9
|
+
* ~~strike~~ → ~strike~
|
|
10
|
+
* - bullet → • bullet
|
|
11
|
+
* # heading → *heading*
|
|
12
|
+
* [text](url) → <url|text>
|
|
13
|
+
*
|
|
14
|
+
* Fenced code (``` … ```) and inline `code` pass through unchanged — they
|
|
15
|
+
* render identically in both flavours.
|
|
16
|
+
*
|
|
17
|
+
* Tables (D15): mrkdwn has no table primitive; we wrap GFM tables in a
|
|
18
|
+
* code fence so they render in monospace and stay readable.
|
|
19
|
+
*/
|
|
20
|
+
export declare function markdownToMrkdwn(input: string): string;
|
|
21
|
+
//# sourceMappingURL=markdown-to-mrkdwn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-to-mrkdwn.d.ts","sourceRoot":"","sources":["../src/markdown-to-mrkdwn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CA6DtD"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate the agent's standard Markdown into Slack's `mrkdwn` flavour.
|
|
3
|
+
*
|
|
4
|
+
* Markdown → mrkdwn
|
|
5
|
+
* **bold** → *bold*
|
|
6
|
+
* __bold__ → *bold*
|
|
7
|
+
* *italic* → _italic_
|
|
8
|
+
* _italic_ → _italic_
|
|
9
|
+
* ~~strike~~ → ~strike~
|
|
10
|
+
* - bullet → • bullet
|
|
11
|
+
* # heading → *heading*
|
|
12
|
+
* [text](url) → <url|text>
|
|
13
|
+
*
|
|
14
|
+
* Fenced code (``` … ```) and inline `code` pass through unchanged — they
|
|
15
|
+
* render identically in both flavours.
|
|
16
|
+
*
|
|
17
|
+
* Tables (D15): mrkdwn has no table primitive; we wrap GFM tables in a
|
|
18
|
+
* code fence so they render in monospace and stay readable.
|
|
19
|
+
*/
|
|
20
|
+
export function markdownToMrkdwn(input) {
|
|
21
|
+
if (!input)
|
|
22
|
+
return input;
|
|
23
|
+
// ── 1. Pull code regions and tables out so we don't touch them. ──
|
|
24
|
+
const codeRegions = [];
|
|
25
|
+
const codePlaceholder = (i) => `CODE${i}`;
|
|
26
|
+
let body = input.replace(/```[\s\S]*?```/g, (match) => {
|
|
27
|
+
codeRegions.push(match);
|
|
28
|
+
return codePlaceholder(codeRegions.length - 1);
|
|
29
|
+
});
|
|
30
|
+
body = body.replace(/`[^`\n]*`/g, (match) => {
|
|
31
|
+
codeRegions.push(match);
|
|
32
|
+
return codePlaceholder(codeRegions.length - 1);
|
|
33
|
+
});
|
|
34
|
+
// GFM-style tables: wrap in a fence with column-aligned cells so they
|
|
35
|
+
// render as a readable monospace table rather than a pile of pipes.
|
|
36
|
+
body = body.replace(/(^\|[^\n]+\|\s*\n\|[\s:|-]+\|\s*\n(?:\|[^\n]+\|\s*\n?)+)/gm, (table) => {
|
|
37
|
+
const fenced = "```\n" + alignTable(table.trimEnd()) + "\n```";
|
|
38
|
+
codeRegions.push(fenced);
|
|
39
|
+
return codePlaceholder(codeRegions.length - 1);
|
|
40
|
+
});
|
|
41
|
+
// ── 2. Bold first, into a sentinel; then italic won't eat its output. ──
|
|
42
|
+
const BOLD_OPEN = "";
|
|
43
|
+
const BOLD_CLOSE = "";
|
|
44
|
+
body = body.replace(/\*\*([^\n*]+?)\*\*/g, `${BOLD_OPEN}$1${BOLD_CLOSE}`);
|
|
45
|
+
body = body.replace(/__([^\n_]+?)__/g, `${BOLD_OPEN}$1${BOLD_CLOSE}`);
|
|
46
|
+
// Headings (#…) → bold (also sentinel-marked).
|
|
47
|
+
body = body.replace(/^\s{0,3}#{1,6}\s+(.*)$/gm, (_m, text) => `${BOLD_OPEN}${text.trim()}${BOLD_CLOSE}`);
|
|
48
|
+
// Strikethrough ~~text~~ → ~text~
|
|
49
|
+
body = body.replace(/~~([^\n~]+?)~~/g, "~$1~");
|
|
50
|
+
// Italic *text* → _text_ (skip already-converted bold sentinels).
|
|
51
|
+
body = body.replace(/(^|[^*\w])\*(\S(?:[^*\n]*\S)?)\*(?!\w)/g, "$1_$2_");
|
|
52
|
+
// Italic _text_ stays _text_ (no-op transform, but ensures the form is canonical).
|
|
53
|
+
// Markdown links [text](url) → <url|text>
|
|
54
|
+
body = body.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, "<$2|$1>");
|
|
55
|
+
// Bullet list markers: `- ` / `* ` / `+ ` at the start of a line → "• "
|
|
56
|
+
body = body.replace(/^(\s*)[-*+]\s+/gm, "$1• ");
|
|
57
|
+
// ── 3. Restore sentinels and code regions. ──
|
|
58
|
+
body = body.replace(new RegExp(BOLD_OPEN, "g"), "*");
|
|
59
|
+
body = body.replace(new RegExp(BOLD_CLOSE, "g"), "*");
|
|
60
|
+
body = body.replace(/CODE(\d+)/g, (_m, idx) => codeRegions[Number(idx)] ?? "");
|
|
61
|
+
return body;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Re-render a GFM table with cells padded to consistent column widths so a
|
|
65
|
+
* monospace render in Slack reads like a real table instead of pipe-soup.
|
|
66
|
+
* Drops the separator row (no visual value in monospace).
|
|
67
|
+
*/
|
|
68
|
+
function alignTable(table) {
|
|
69
|
+
const lines = table.split("\n").filter((l) => l.trim().length > 0);
|
|
70
|
+
const rows = lines.map((line) => {
|
|
71
|
+
const cells = line.split("|").map((c) => c.trim());
|
|
72
|
+
if (cells.length > 0 && cells[0] === "")
|
|
73
|
+
cells.shift();
|
|
74
|
+
if (cells.length > 0 && cells[cells.length - 1] === "")
|
|
75
|
+
cells.pop();
|
|
76
|
+
return cells;
|
|
77
|
+
});
|
|
78
|
+
const isSeparator = (row) => row.length > 0 && row.every((c) => /^[-:\s]+$/.test(c));
|
|
79
|
+
const dataRows = rows.filter((r) => !isSeparator(r));
|
|
80
|
+
if (dataRows.length === 0)
|
|
81
|
+
return table;
|
|
82
|
+
const colCount = Math.max(...dataRows.map((r) => r.length));
|
|
83
|
+
const widths = [];
|
|
84
|
+
for (let c = 0; c < colCount; c++) {
|
|
85
|
+
let max = 0;
|
|
86
|
+
for (const row of dataRows) {
|
|
87
|
+
const cell = row[c] ?? "";
|
|
88
|
+
if (cell.length > max)
|
|
89
|
+
max = cell.length;
|
|
90
|
+
}
|
|
91
|
+
widths.push(max);
|
|
92
|
+
}
|
|
93
|
+
return dataRows
|
|
94
|
+
.map((row) => {
|
|
95
|
+
const padded = widths.map((w, c) => (row[c] ?? "").padEnd(w));
|
|
96
|
+
return "| " + padded.join(" | ") + " |";
|
|
97
|
+
})
|
|
98
|
+
.join("\n");
|
|
99
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-Slack-message streaming text state machine.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a single posted Slack message and lets callers feed it the growing
|
|
5
|
+
* text buffer of a streaming reply. Edits are throttled (Slack's chat.update
|
|
6
|
+
* rate limit is ~1/sec per message) and *serialised* through a per-message
|
|
7
|
+
* promise queue so concurrent updates can't race — a hazard the previous
|
|
8
|
+
* implementation hit, where End-triggered flush of "ALPHA" could be
|
|
9
|
+
* overtaken by an in-flight flush of "AL" and the final state read "AL".
|
|
10
|
+
*
|
|
11
|
+
* Nothing in this file knows about AG-UI events or @slack/bolt — it's a
|
|
12
|
+
* pure Slack-side primitive: "give me text, I'll keep one Slack message
|
|
13
|
+
* in sync with it."
|
|
14
|
+
*/
|
|
15
|
+
export interface MessageStreamConfig {
|
|
16
|
+
/** Function that actually writes `text` to Slack (e.g. chat.update). */
|
|
17
|
+
update: (text: string) => Promise<void>;
|
|
18
|
+
/** Minimum gap between consecutive flushes, in ms (defaults to 800). */
|
|
19
|
+
minIntervalMs?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare class MessageStream {
|
|
22
|
+
private buffer;
|
|
23
|
+
private posted;
|
|
24
|
+
private queue;
|
|
25
|
+
private lastFlushedAt;
|
|
26
|
+
private flushTimer;
|
|
27
|
+
private readonly minIntervalMs;
|
|
28
|
+
private readonly update;
|
|
29
|
+
constructor(config: MessageStreamConfig);
|
|
30
|
+
/** Replace the in-flight buffer (callers pass the accumulated text). */
|
|
31
|
+
append(text: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* Mark the stream done. Cancels any pending throttled flush, enqueues a
|
|
34
|
+
* final flush, and resolves once the entire queue (including the final
|
|
35
|
+
* flush and anything previously in flight) has drained.
|
|
36
|
+
*
|
|
37
|
+
* After this resolves, the Slack message reflects the final buffer state.
|
|
38
|
+
*/
|
|
39
|
+
finish(): Promise<void>;
|
|
40
|
+
private scheduleFlush;
|
|
41
|
+
private enqueueFlush;
|
|
42
|
+
private flushNow;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=message-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-stream.d.ts","sourceRoot":"","sources":["../src/message-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,mBAAmB;IAClC,wEAAwE;IACxE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAID,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkC;gBAE7C,MAAM,EAAE,mBAAmB;IAKvC,wEAAwE;IACxE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM1B;;;;;;OAMG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAS7B,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,YAAY;YAIN,QAAQ;CAkBvB"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const DEFAULT_MIN_INTERVAL_MS = 800;
|
|
2
|
+
export class MessageStream {
|
|
3
|
+
buffer = "";
|
|
4
|
+
posted = "";
|
|
5
|
+
queue = Promise.resolve();
|
|
6
|
+
lastFlushedAt = 0;
|
|
7
|
+
flushTimer;
|
|
8
|
+
minIntervalMs;
|
|
9
|
+
update;
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.update = config.update;
|
|
12
|
+
this.minIntervalMs = config.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
13
|
+
}
|
|
14
|
+
/** Replace the in-flight buffer (callers pass the accumulated text). */
|
|
15
|
+
append(text) {
|
|
16
|
+
if (text === this.buffer)
|
|
17
|
+
return;
|
|
18
|
+
this.buffer = text;
|
|
19
|
+
this.scheduleFlush();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Mark the stream done. Cancels any pending throttled flush, enqueues a
|
|
23
|
+
* final flush, and resolves once the entire queue (including the final
|
|
24
|
+
* flush and anything previously in flight) has drained.
|
|
25
|
+
*
|
|
26
|
+
* After this resolves, the Slack message reflects the final buffer state.
|
|
27
|
+
*/
|
|
28
|
+
async finish() {
|
|
29
|
+
if (this.flushTimer) {
|
|
30
|
+
clearTimeout(this.flushTimer);
|
|
31
|
+
this.flushTimer = undefined;
|
|
32
|
+
}
|
|
33
|
+
this.enqueueFlush();
|
|
34
|
+
await this.queue;
|
|
35
|
+
}
|
|
36
|
+
scheduleFlush() {
|
|
37
|
+
if (this.flushTimer)
|
|
38
|
+
return;
|
|
39
|
+
const elapsed = Date.now() - this.lastFlushedAt;
|
|
40
|
+
const delay = Math.max(0, this.minIntervalMs - elapsed);
|
|
41
|
+
this.flushTimer = setTimeout(() => {
|
|
42
|
+
this.flushTimer = undefined;
|
|
43
|
+
this.enqueueFlush();
|
|
44
|
+
}, delay);
|
|
45
|
+
}
|
|
46
|
+
enqueueFlush() {
|
|
47
|
+
this.queue = this.queue.then(() => this.flushNow());
|
|
48
|
+
}
|
|
49
|
+
async flushNow() {
|
|
50
|
+
if (this.buffer === this.posted)
|
|
51
|
+
return;
|
|
52
|
+
const text = this.buffer;
|
|
53
|
+
this.posted = text;
|
|
54
|
+
try {
|
|
55
|
+
await this.update(text);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
// Rate-limit (429) edits are already retried by the Slack WebClient
|
|
59
|
+
// (honoring Retry-After). If we still land here the edit genuinely
|
|
60
|
+
// failed — swallow it: a single failed edit shouldn't sink the
|
|
61
|
+
// stream, and future appends retry with the latest buffer.
|
|
62
|
+
console.error("[message-stream] update failed:", err);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
// Set lastFlushedAt *after* the update returns so the throttle
|
|
66
|
+
// measures wall-clock time between completions, not starts.
|
|
67
|
+
this.lastFlushedAt = Date.now();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Slack streaming transport (`chat.startStream` / `appendStream` /
|
|
3
|
+
* `stopStream`, GA Oct 2025) behind the SAME `append(fullText)/finish()`
|
|
4
|
+
* contract as the shipped {@link MessageStream} — callers (the event-renderer's
|
|
5
|
+
* reply stream, `adapter.stream()`) can't tell which transport ran.
|
|
6
|
+
*
|
|
7
|
+
* Differences from the legacy `chat.update` streamer:
|
|
8
|
+
*
|
|
9
|
+
* - Slack renders a true streaming UI, and the payload is **raw markdown**
|
|
10
|
+
* (`markdown_text`), so real tables / fenced code render natively — there
|
|
11
|
+
* is NO mrkdwn translation and NO bracket auto-closing (Slack's streaming
|
|
12
|
+
* renderer tolerates a mid-stream-unbalanced buffer).
|
|
13
|
+
* - `appendStream` takes the *delta* since the last flush, not the full
|
|
14
|
+
* accumulated text, so this class tracks how much it has already sent.
|
|
15
|
+
* - A single streamed message holds the whole reply: Slack documents no
|
|
16
|
+
* cumulative per-message cap, only a **12k char limit per `markdown_text`
|
|
17
|
+
* call**, so long replies are sent as successive ≤12k appends to the SAME
|
|
18
|
+
* message (no multi-message splitting — that was a `chat.update`-era
|
|
19
|
+
* workaround).
|
|
20
|
+
* - Beyond text, the stream can carry structured {@link AnyChunk}s
|
|
21
|
+
* (`task_update` / `plan_update` / `blocks`) via {@link appendChunk}, which
|
|
22
|
+
* flushes any pending text first so ordering is preserved, and a finalized
|
|
23
|
+
* message can carry trailing Block Kit (e.g. a feedback row) via the
|
|
24
|
+
* `finalBlocks` passed to {@link finish}.
|
|
25
|
+
*
|
|
26
|
+
* Failure handling — "opting in can never break a bot": if the very first
|
|
27
|
+
* `startStream` throws (e.g. a workspace where the streaming API is
|
|
28
|
+
* unavailable), the stream transparently rebuilds itself on the supplied
|
|
29
|
+
* legacy `fallback()` transport and replays the buffer there. `onStartFailure`
|
|
30
|
+
* lets the adapter mark the workspace legacy so subsequent streams skip the
|
|
31
|
+
* native path entirely. Per-`appendStream` failures mid-stream are swallowed
|
|
32
|
+
* (logged) like the legacy streamer's failed edits; a failing structured-chunk
|
|
33
|
+
* append additionally fires `onChunkFailure` so the caller can degrade
|
|
34
|
+
* tool-progress to its legacy surface.
|
|
35
|
+
*
|
|
36
|
+
* Nothing here imports `@slack/web-api` — the Slack calls are injected as a
|
|
37
|
+
* {@link NativeStreamTransport}, keeping the cadence logic unit-testable with
|
|
38
|
+
* fake timers and a fake transport.
|
|
39
|
+
*/
|
|
40
|
+
import type { AnyChunk, KnownBlock } from "@slack/types";
|
|
41
|
+
/** A minimal `{ append(fullText), finish() }` streaming sink. */
|
|
42
|
+
export interface TextStream {
|
|
43
|
+
/** Replace the in-flight buffer with the accumulated text. */
|
|
44
|
+
append(fullText: string): void;
|
|
45
|
+
/** Flush the final state and close the stream. */
|
|
46
|
+
finish(): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/** The Slack streaming calls, injected so this file stays SDK-free. */
|
|
49
|
+
export interface NativeStreamTransport {
|
|
50
|
+
/** `chat.startStream` → resolves with the new streamed message's `ts`. Throws on failure. */
|
|
51
|
+
startStream(): Promise<string>;
|
|
52
|
+
/** `chat.appendStream` — append a raw `markdown_text` delta to the message at `ts`. */
|
|
53
|
+
appendText(ts: string, markdownText: string): Promise<void>;
|
|
54
|
+
/** `chat.appendStream` — append structured {@link AnyChunk}s to the message at `ts`. */
|
|
55
|
+
appendChunks(ts: string, chunks: AnyChunk[]): Promise<void>;
|
|
56
|
+
/** `chat.stopStream` — finalize the streamed message at `ts`, optionally with trailing blocks. */
|
|
57
|
+
stopStream(ts: string, finalBlocks?: KnownBlock[]): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
export interface NativeMessageStreamConfig {
|
|
60
|
+
transport: NativeStreamTransport;
|
|
61
|
+
/**
|
|
62
|
+
* Builds the legacy `chat.update` transport, used only if the first
|
|
63
|
+
* `startStream` throws. The accumulated buffer is replayed into it so no
|
|
64
|
+
* text is lost.
|
|
65
|
+
*/
|
|
66
|
+
fallback: () => TextStream;
|
|
67
|
+
/** Called once when the first `startStream` fails (adapter marks the workspace legacy). */
|
|
68
|
+
onStartFailure?: (err: unknown) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Called when a structured-chunk append fails or is impossible (the stream
|
|
71
|
+
* has already fallen back to the legacy `chat.update` transport, which has no
|
|
72
|
+
* chunk equivalent). Lets the caller degrade tool-progress to its legacy
|
|
73
|
+
* surface (`:wrench:` rows). Text streaming is unaffected.
|
|
74
|
+
*/
|
|
75
|
+
onChunkFailure?: (err: unknown) => void;
|
|
76
|
+
/** Minimum gap between text flushes, in ms (defaults to 600). */
|
|
77
|
+
minIntervalMs?: number;
|
|
78
|
+
}
|
|
79
|
+
export declare class NativeMessageStream implements TextStream {
|
|
80
|
+
private buffer;
|
|
81
|
+
private queue;
|
|
82
|
+
private lastFlushedAt;
|
|
83
|
+
private flushTimer;
|
|
84
|
+
private finished;
|
|
85
|
+
/** Current streamed message ts (undefined until the first `startStream`). */
|
|
86
|
+
private curTs;
|
|
87
|
+
/** Buffer chars already appended as text to the current message. */
|
|
88
|
+
private curPosted;
|
|
89
|
+
/** ts of the first streamed message (for the returned MessageRef). */
|
|
90
|
+
private firstTsValue;
|
|
91
|
+
/** Set once `startStream` has failed and we've fallen back to the legacy transport. */
|
|
92
|
+
private legacy;
|
|
93
|
+
/** Set once a chunk append has failed/been refused, so we stop trying. */
|
|
94
|
+
private chunksDisabled;
|
|
95
|
+
private readonly transport;
|
|
96
|
+
private readonly makeFallback;
|
|
97
|
+
private readonly onStartFailure;
|
|
98
|
+
private readonly onChunkFailure;
|
|
99
|
+
private readonly minIntervalMs;
|
|
100
|
+
constructor(config: NativeMessageStreamConfig);
|
|
101
|
+
/** The first streamed message's ts (or the fallback's), available after finish(). */
|
|
102
|
+
get firstTs(): string | undefined;
|
|
103
|
+
append(fullText: string): void;
|
|
104
|
+
/**
|
|
105
|
+
* Append a structured chunk (`task_update` / `plan_update` / `blocks`) to the
|
|
106
|
+
* streamed message. Flushes any pending text first so the chunk lands AFTER
|
|
107
|
+
* the text emitted so far. No-op (firing `onChunkFailure` once) when the
|
|
108
|
+
* stream has fallen back to legacy or chunks were already refused.
|
|
109
|
+
*/
|
|
110
|
+
appendChunk(chunk: AnyChunk): void;
|
|
111
|
+
finish(finalBlocks?: KnownBlock[]): Promise<void>;
|
|
112
|
+
private scheduleFlush;
|
|
113
|
+
private enqueueFlush;
|
|
114
|
+
/** Ensure the stream is started; on failure, fall back to legacy and replay. Returns false if failed over. */
|
|
115
|
+
private ensureStarted;
|
|
116
|
+
/** Append all un-posted buffer text to the current message, chunked under the 12k per-call cap. */
|
|
117
|
+
private flushText;
|
|
118
|
+
/** Flush pending text, then append one structured chunk. */
|
|
119
|
+
private flushChunk;
|
|
120
|
+
/** Append pending text to the current (already-started) message; swallow failures. */
|
|
121
|
+
private flushTextInline;
|
|
122
|
+
private disableChunks;
|
|
123
|
+
/**
|
|
124
|
+
* Switch to the legacy `chat.update` transport and replay the full buffer so
|
|
125
|
+
* no text is lost ("opting in can never break a bot"). The full buffer is
|
|
126
|
+
* replayed because `append()` forwards the accumulated full text once
|
|
127
|
+
* `this.legacy` is set, so the legacy stream owns the whole response.
|
|
128
|
+
*/
|
|
129
|
+
private failOverToLegacy;
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=native-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-stream.d.ts","sourceRoot":"","sources":["../src/native-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAEzD,iEAAiE;AACjE,MAAM,WAAW,UAAU;IACzB,8DAA8D;IAC9D,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,kDAAkD;IAClD,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAED,uEAAuE;AACvE,MAAM,WAAW,qBAAqB;IACpC,6FAA6F;IAC7F,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/B,uFAAuF;IACvF,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,wFAAwF;IACxF,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,kGAAkG;IAClG,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,qBAAqB,CAAC;IACjC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,UAAU,CAAC;IAC3B,2FAA2F;IAC3F,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,iEAAiE;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAYD,qBAAa,mBAAoB,YAAW,UAAU;IACpD,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,UAAU,CAA4C;IAC9D,OAAO,CAAC,QAAQ,CAAS;IAEzB,6EAA6E;IAC7E,OAAO,CAAC,KAAK,CAAqB;IAClC,oEAAoE;IACpE,OAAO,CAAC,SAAS,CAAK;IACtB,sEAAsE;IACtE,OAAO,CAAC,YAAY,CAAqB;IAEzC,uFAAuF;IACvF,OAAO,CAAC,MAAM,CAAyB;IACvC,0EAA0E;IAC1E,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAClD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmB;IAChD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAuC;IACtE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAuC;IACtE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAE3B,MAAM,EAAE,yBAAyB;IAQ7C,qFAAqF;IACrF,IAAI,OAAO,IAAI,MAAM,GAAG,SAAS,CAEhC;IAED,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAU9B;;;;;OAKG;IACH,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;IAiB5B,MAAM,CAAC,WAAW,CAAC,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBvD,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,YAAY;IAIpB,8GAA8G;YAChG,aAAa;IAY3B,mGAAmG;YACrF,SAAS;IA2BvB,4DAA4D;YAC9C,UAAU;IAuBxB,sFAAsF;YACxE,eAAe;IAqB7B,OAAO,CAAC,aAAa;IAMrB;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;CAUzB"}
|