@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,289 @@
|
|
|
1
|
+
import { Header, Message, Section, renderToIR } from "@copilotkit/channels-ui";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { renderBlockKit, renderSlackMessage } from "./block-kit.js";
|
|
4
|
+
describe("renderBlockKit", () => {
|
|
5
|
+
it("flattens a message into header + section blocks (markdown → mrkdwn)", () => {
|
|
6
|
+
const ir = renderToIR(Message({
|
|
7
|
+
children: [
|
|
8
|
+
Header({ children: "Hi" }),
|
|
9
|
+
Section({ children: "**bold**" }),
|
|
10
|
+
],
|
|
11
|
+
}));
|
|
12
|
+
expect(renderBlockKit(ir)).toEqual([
|
|
13
|
+
{ type: "header", text: { type: "plain_text", text: "Hi" } },
|
|
14
|
+
{ type: "section", text: { type: "mrkdwn", text: "*bold*" } },
|
|
15
|
+
]);
|
|
16
|
+
});
|
|
17
|
+
it("renders a pre-bound button inside actions with its stamped action_id", () => {
|
|
18
|
+
const ir = [
|
|
19
|
+
{
|
|
20
|
+
type: "actions",
|
|
21
|
+
props: {
|
|
22
|
+
children: [
|
|
23
|
+
{
|
|
24
|
+
type: "button",
|
|
25
|
+
props: {
|
|
26
|
+
onClick: { id: "ck:abc" },
|
|
27
|
+
value: { confirmed: true },
|
|
28
|
+
style: "primary",
|
|
29
|
+
children: [{ type: "text", props: { value: "Create" } }],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
expect(renderBlockKit(ir)).toEqual([
|
|
37
|
+
{
|
|
38
|
+
type: "actions",
|
|
39
|
+
elements: [
|
|
40
|
+
{
|
|
41
|
+
type: "button",
|
|
42
|
+
action_id: "ck:abc",
|
|
43
|
+
text: { type: "plain_text", text: "Create" },
|
|
44
|
+
value: '{"confirmed":true}',
|
|
45
|
+
style: "primary",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
it("renders a divider", () => {
|
|
52
|
+
expect(renderBlockKit([{ type: "divider", props: {} }])).toEqual([
|
|
53
|
+
{ type: "divider" },
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
it("applies the section text budget (≤3000, trailing ellipsis)", () => {
|
|
57
|
+
const blocks = renderBlockKit(renderToIR(Section({ children: "x".repeat(4000) })));
|
|
58
|
+
const section = blocks[0];
|
|
59
|
+
expect(section.text.text.length).toBeLessThanOrEqual(3000);
|
|
60
|
+
expect(section.text.text.endsWith("…")).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
it("renders an input block with its stamped action_id", () => {
|
|
63
|
+
expect(renderBlockKit([
|
|
64
|
+
{
|
|
65
|
+
type: "input",
|
|
66
|
+
props: {
|
|
67
|
+
onSubmit: { id: "ck:in1" },
|
|
68
|
+
placeholder: "Name",
|
|
69
|
+
multiline: false,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
])).toEqual([
|
|
73
|
+
{
|
|
74
|
+
type: "input",
|
|
75
|
+
dispatch_action: true,
|
|
76
|
+
element: {
|
|
77
|
+
type: "plain_text_input",
|
|
78
|
+
action_id: "ck:in1",
|
|
79
|
+
multiline: false,
|
|
80
|
+
},
|
|
81
|
+
label: { type: "plain_text", text: "Name" },
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
it("gives a static_select a fallback action_id when onSelect is absent", () => {
|
|
86
|
+
const blocks = renderBlockKit([
|
|
87
|
+
{
|
|
88
|
+
type: "actions",
|
|
89
|
+
props: {
|
|
90
|
+
children: [
|
|
91
|
+
{
|
|
92
|
+
type: "select",
|
|
93
|
+
props: { options: [{ label: "A", value: "a" }] },
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
const select = blocks[0]
|
|
100
|
+
.elements[0];
|
|
101
|
+
expect(select.action_id).toBe("select");
|
|
102
|
+
expect(select.action_id.length).toBeGreaterThan(0);
|
|
103
|
+
});
|
|
104
|
+
it("renders a Table IR into a native Slack table block", () => {
|
|
105
|
+
const ir = [
|
|
106
|
+
{
|
|
107
|
+
type: "table",
|
|
108
|
+
props: {
|
|
109
|
+
columns: [{ header: "Name" }, { header: "Status", align: "center" }],
|
|
110
|
+
children: [
|
|
111
|
+
{
|
|
112
|
+
type: "row",
|
|
113
|
+
props: {
|
|
114
|
+
children: [
|
|
115
|
+
{
|
|
116
|
+
type: "cell",
|
|
117
|
+
props: {
|
|
118
|
+
children: [{ type: "text", props: { value: "Ana" } }],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: "cell",
|
|
123
|
+
props: {
|
|
124
|
+
children: [{ type: "text", props: { value: "Active" } }],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
expect(renderBlockKit(ir)).toEqual([
|
|
135
|
+
{
|
|
136
|
+
type: "table",
|
|
137
|
+
rows: [
|
|
138
|
+
[
|
|
139
|
+
{ type: "raw_text", text: "Name" },
|
|
140
|
+
{ type: "raw_text", text: "Status" },
|
|
141
|
+
],
|
|
142
|
+
[
|
|
143
|
+
{ type: "raw_text", text: "Ana" },
|
|
144
|
+
{ type: "raw_text", text: "Active" },
|
|
145
|
+
],
|
|
146
|
+
],
|
|
147
|
+
column_settings: [{ align: "left" }, { align: "center" }],
|
|
148
|
+
},
|
|
149
|
+
]);
|
|
150
|
+
});
|
|
151
|
+
it("passes raw native Block Kit through unchanged", () => {
|
|
152
|
+
expect(renderBlockKit([
|
|
153
|
+
{
|
|
154
|
+
type: "raw",
|
|
155
|
+
props: {
|
|
156
|
+
value: [
|
|
157
|
+
{ type: "section", text: { type: "mrkdwn", text: "native" } },
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
])).toEqual([{ type: "section", text: { type: "mrkdwn", text: "native" } }]);
|
|
162
|
+
});
|
|
163
|
+
it("renders a link button with a url", () => {
|
|
164
|
+
const blocks = renderBlockKit([
|
|
165
|
+
{
|
|
166
|
+
type: "actions",
|
|
167
|
+
props: {
|
|
168
|
+
children: [
|
|
169
|
+
{
|
|
170
|
+
type: "button",
|
|
171
|
+
props: {
|
|
172
|
+
url: "https://dash/deploy/42",
|
|
173
|
+
children: [{ type: "text", props: { value: "Open" } }],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
]);
|
|
180
|
+
const el = blocks[0].elements[0];
|
|
181
|
+
expect(el.url).toBe("https://dash/deploy/42");
|
|
182
|
+
});
|
|
183
|
+
it("renders a Field label as a bold mrkdwn line above the value", () => {
|
|
184
|
+
const blocks = renderBlockKit([
|
|
185
|
+
{
|
|
186
|
+
type: "field",
|
|
187
|
+
props: {
|
|
188
|
+
label: "Status",
|
|
189
|
+
children: [{ type: "text", props: { value: "Online" } }],
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
]);
|
|
193
|
+
const text = blocks[0].fields[0].text;
|
|
194
|
+
expect(text).toBe("*Status*\nOnline");
|
|
195
|
+
});
|
|
196
|
+
it("renders a multi-select as its own input block, not inside actions", () => {
|
|
197
|
+
const blocks = renderBlockKit([
|
|
198
|
+
{
|
|
199
|
+
type: "actions",
|
|
200
|
+
props: {
|
|
201
|
+
children: [
|
|
202
|
+
{
|
|
203
|
+
type: "select",
|
|
204
|
+
props: {
|
|
205
|
+
multi: true,
|
|
206
|
+
onSelect: { id: "ck:ms" },
|
|
207
|
+
placeholder: "Pick teams",
|
|
208
|
+
options: [
|
|
209
|
+
{ label: "Core", value: "core" },
|
|
210
|
+
{ label: "Infra", value: "infra" },
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
]);
|
|
218
|
+
// No actions block is emitted (the only child was peeled into an input block).
|
|
219
|
+
expect(blocks).toHaveLength(1);
|
|
220
|
+
const block = blocks[0];
|
|
221
|
+
expect(block.type).toBe("input");
|
|
222
|
+
expect(block.dispatch_action).toBe(true);
|
|
223
|
+
expect(block.element.type).toBe("multi_static_select");
|
|
224
|
+
expect(block.element.action_id).toBe("ck:ms");
|
|
225
|
+
});
|
|
226
|
+
it("keeps source order when a multi-select is mixed with a button", () => {
|
|
227
|
+
const blocks = renderBlockKit([
|
|
228
|
+
{
|
|
229
|
+
type: "actions",
|
|
230
|
+
props: {
|
|
231
|
+
children: [
|
|
232
|
+
{
|
|
233
|
+
type: "button",
|
|
234
|
+
props: {
|
|
235
|
+
onClick: { id: "ck:b" },
|
|
236
|
+
children: [{ type: "text", props: { value: "Go" } }],
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
type: "select",
|
|
241
|
+
props: {
|
|
242
|
+
multi: true,
|
|
243
|
+
onSelect: { id: "ck:ms" },
|
|
244
|
+
options: [{ label: "Core", value: "core" }],
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
]);
|
|
251
|
+
// The button's actions block comes first, then the multi-select input block.
|
|
252
|
+
expect(blocks.map((b) => b.type)).toEqual([
|
|
253
|
+
"actions",
|
|
254
|
+
"input",
|
|
255
|
+
]);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
describe("renderSlackMessage", () => {
|
|
259
|
+
it("extracts a top-level <Message accent> as the attachment color", () => {
|
|
260
|
+
expect(renderSlackMessage([
|
|
261
|
+
{
|
|
262
|
+
type: "message",
|
|
263
|
+
props: {
|
|
264
|
+
accent: "#EB5757",
|
|
265
|
+
children: [
|
|
266
|
+
{
|
|
267
|
+
type: "section",
|
|
268
|
+
props: { children: [{ type: "text", props: { value: "hi" } }] },
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
])).toEqual({
|
|
274
|
+
blocks: [{ type: "section", text: { type: "mrkdwn", text: "hi" } }],
|
|
275
|
+
accent: "#EB5757",
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
it("returns no accent when there is no message wrapper", () => {
|
|
279
|
+
expect(renderSlackMessage([
|
|
280
|
+
{
|
|
281
|
+
type: "section",
|
|
282
|
+
props: { children: [{ type: "text", props: { value: "hi" } }] },
|
|
283
|
+
},
|
|
284
|
+
])).toEqual({
|
|
285
|
+
blocks: [{ type: "section", text: { type: "mrkdwn", text: "hi" } }],
|
|
286
|
+
accent: undefined,
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const SLACK_LIMITS: {
|
|
2
|
+
readonly blocksPerMessage: 50;
|
|
3
|
+
readonly sectionText: 3000;
|
|
4
|
+
readonly headerText: 150;
|
|
5
|
+
readonly fieldsPerSection: 10;
|
|
6
|
+
readonly fieldText: 2000;
|
|
7
|
+
readonly actionsElements: 25;
|
|
8
|
+
readonly contextElements: 10;
|
|
9
|
+
readonly buttonText: 75;
|
|
10
|
+
readonly actionId: 255;
|
|
11
|
+
readonly buttonValue: 2000;
|
|
12
|
+
readonly selectOptions: 100;
|
|
13
|
+
readonly tableColumns: 20;
|
|
14
|
+
readonly tableRows: 100;
|
|
15
|
+
readonly cellText: 2000;
|
|
16
|
+
};
|
|
17
|
+
/** Truncate to max chars, appending an ellipsis marker if the input was longer. Never returns >max. */
|
|
18
|
+
export declare function truncateText(text: string, max: number): string;
|
|
19
|
+
/** Clamp an array to max items; return the kept items plus how many overflowed (for an overflow signal). */
|
|
20
|
+
export declare function clampArray<T>(items: readonly T[], max: number): {
|
|
21
|
+
items: T[];
|
|
22
|
+
overflow: number;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.d.ts","sourceRoot":"","sources":["../../src/render/budget.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;CAef,CAAC;AAEX,uGAAuG;AACvG,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAI9D;AAED,4GAA4G;AAC5G,wBAAgB,UAAU,CAAC,CAAC,EAC1B,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,GAAG,EAAE,MAAM,GACV;IAAE,KAAK,EAAE,CAAC,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAGlC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const SLACK_LIMITS = {
|
|
2
|
+
blocksPerMessage: 50,
|
|
3
|
+
sectionText: 3000,
|
|
4
|
+
headerText: 150,
|
|
5
|
+
fieldsPerSection: 10,
|
|
6
|
+
fieldText: 2000,
|
|
7
|
+
actionsElements: 25,
|
|
8
|
+
contextElements: 10,
|
|
9
|
+
buttonText: 75,
|
|
10
|
+
actionId: 255,
|
|
11
|
+
buttonValue: 2000,
|
|
12
|
+
selectOptions: 100,
|
|
13
|
+
tableColumns: 20,
|
|
14
|
+
tableRows: 100,
|
|
15
|
+
cellText: 2000,
|
|
16
|
+
};
|
|
17
|
+
/** Truncate to max chars, appending an ellipsis marker if the input was longer. Never returns >max. */
|
|
18
|
+
export function truncateText(text, max) {
|
|
19
|
+
if (text.length <= max)
|
|
20
|
+
return text;
|
|
21
|
+
if (max <= 1)
|
|
22
|
+
return text.slice(0, max);
|
|
23
|
+
return text.slice(0, max - 1) + "…"; // …
|
|
24
|
+
}
|
|
25
|
+
/** Clamp an array to max items; return the kept items plus how many overflowed (for an overflow signal). */
|
|
26
|
+
export function clampArray(items, max) {
|
|
27
|
+
if (items.length <= max)
|
|
28
|
+
return { items: [...items], overflow: 0 };
|
|
29
|
+
return { items: items.slice(0, max), overflow: items.length - max };
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.test.d.ts","sourceRoot":"","sources":["../../src/render/budget.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { SLACK_LIMITS, truncateText, clampArray } from "./budget.js";
|
|
3
|
+
describe("budget: truncateText", () => {
|
|
4
|
+
it("leaves short text unchanged", () => {
|
|
5
|
+
expect(truncateText("hello", 3000)).toBe("hello");
|
|
6
|
+
});
|
|
7
|
+
it("truncates section text to 3000 chars with an ellipsis", () => {
|
|
8
|
+
const long = "x".repeat(4000);
|
|
9
|
+
const out = truncateText(long, SLACK_LIMITS.sectionText);
|
|
10
|
+
expect(out.length).toBe(3000);
|
|
11
|
+
expect(out.endsWith("…")).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
it("truncates button text to 75 and action_id to 255", () => {
|
|
14
|
+
expect(truncateText("y".repeat(200), SLACK_LIMITS.buttonText).length).toBe(75);
|
|
15
|
+
expect(truncateText("z".repeat(500), SLACK_LIMITS.actionId).length).toBe(255);
|
|
16
|
+
});
|
|
17
|
+
it("truncates field text to 2000 and button value to 2000", () => {
|
|
18
|
+
expect(truncateText("a".repeat(5000), SLACK_LIMITS.fieldText).length).toBe(2000);
|
|
19
|
+
expect(truncateText("b".repeat(5000), SLACK_LIMITS.buttonValue).length).toBe(2000);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe("budget: clampArray", () => {
|
|
23
|
+
it("keeps arrays within the limit untouched (overflow 0)", () => {
|
|
24
|
+
expect(clampArray([1, 2, 3], 25)).toEqual({
|
|
25
|
+
items: [1, 2, 3],
|
|
26
|
+
overflow: 0,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
it("clamps actions to 25 elements and reports overflow", () => {
|
|
30
|
+
const els = Array.from({ length: 30 }, (_, i) => i);
|
|
31
|
+
const r = clampArray(els, SLACK_LIMITS.actionsElements);
|
|
32
|
+
expect(r.items.length).toBe(25);
|
|
33
|
+
expect(r.overflow).toBe(5);
|
|
34
|
+
});
|
|
35
|
+
it("clamps context to 10, fields to 10, blocks to 50, select options to 100", () => {
|
|
36
|
+
expect(clampArray(Array(20).fill(0), SLACK_LIMITS.contextElements).items.length).toBe(10);
|
|
37
|
+
expect(clampArray(Array(15).fill(0), SLACK_LIMITS.fieldsPerSection).items.length).toBe(10);
|
|
38
|
+
expect(clampArray(Array(60).fill(0), SLACK_LIMITS.blocksPerMessage).items.length).toBe(50);
|
|
39
|
+
expect(clampArray(Array(150).fill(0), SLACK_LIMITS.selectOptions).items.length).toBe(100);
|
|
40
|
+
});
|
|
41
|
+
it("clamps table columns to 20 and table rows to 100", () => {
|
|
42
|
+
const cols = clampArray(Array(30).fill(0), SLACK_LIMITS.tableColumns);
|
|
43
|
+
expect(cols.items.length).toBe(20);
|
|
44
|
+
expect(cols.overflow).toBe(10);
|
|
45
|
+
const rows = clampArray(Array(150).fill(0), SLACK_LIMITS.tableRows);
|
|
46
|
+
expect(rows.items.length).toBe(100);
|
|
47
|
+
expect(rows.overflow).toBe(50);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BotNode } from "@copilotkit/channels-ui";
|
|
2
|
+
import type { View } from "@slack/web-api";
|
|
3
|
+
/**
|
|
4
|
+
* Render a modal IR tree (a `<Modal>` root with `modal_text_input` /
|
|
5
|
+
* `modal_select` / `modal_radio` children) into a Slack `views.open` `View`.
|
|
6
|
+
*
|
|
7
|
+
* Each field's `id` becomes both the block `block_id` and the element
|
|
8
|
+
* `action_id`, so a `view_submission`'s `state.values[id][id]` maps straight
|
|
9
|
+
* back to the field. Throws {@link ModalRenderError} when there is no modal
|
|
10
|
+
* root or a child uses an unsupported element type.
|
|
11
|
+
*/
|
|
12
|
+
export declare function renderSlackModal(ir: BotNode[]): View;
|
|
13
|
+
//# sourceMappingURL=modal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["../../src/render/modal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAEvD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAkB3C;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,IAAI,CA0FpD"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ModalRenderError } from "@copilotkit/channels-ui";
|
|
2
|
+
const plain = (text) => ({ type: "plain_text", text });
|
|
3
|
+
function optionFrom(node) {
|
|
4
|
+
return {
|
|
5
|
+
text: plain(String(node.props.label ?? "")),
|
|
6
|
+
value: String(node.props.value ?? ""),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function childOptions(node) {
|
|
10
|
+
const kids = node.props.children;
|
|
11
|
+
return Array.isArray(kids)
|
|
12
|
+
? kids.filter((k) => k.type === "modal_select_option")
|
|
13
|
+
: [];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Render a modal IR tree (a `<Modal>` root with `modal_text_input` /
|
|
17
|
+
* `modal_select` / `modal_radio` children) into a Slack `views.open` `View`.
|
|
18
|
+
*
|
|
19
|
+
* Each field's `id` becomes both the block `block_id` and the element
|
|
20
|
+
* `action_id`, so a `view_submission`'s `state.values[id][id]` maps straight
|
|
21
|
+
* back to the field. Throws {@link ModalRenderError} when there is no modal
|
|
22
|
+
* root or a child uses an unsupported element type.
|
|
23
|
+
*/
|
|
24
|
+
export function renderSlackModal(ir) {
|
|
25
|
+
const root = ir.find((n) => n.type === "modal");
|
|
26
|
+
if (!root)
|
|
27
|
+
throw new ModalRenderError("renderSlackModal: no <Modal> root in IR");
|
|
28
|
+
const p = root.props;
|
|
29
|
+
const kids = Array.isArray(p.children) ? p.children : [];
|
|
30
|
+
const blocks = kids.map((node) => {
|
|
31
|
+
const fp = node.props;
|
|
32
|
+
const id = String(fp.id ?? "");
|
|
33
|
+
const label = plain(String(fp.label ?? ""));
|
|
34
|
+
const optional = fp.optional === true;
|
|
35
|
+
switch (node.type) {
|
|
36
|
+
case "modal_text_input":
|
|
37
|
+
return {
|
|
38
|
+
type: "input",
|
|
39
|
+
block_id: id,
|
|
40
|
+
label,
|
|
41
|
+
optional,
|
|
42
|
+
element: {
|
|
43
|
+
type: "plain_text_input",
|
|
44
|
+
action_id: id,
|
|
45
|
+
...(fp.multiline ? { multiline: true } : {}),
|
|
46
|
+
...(fp.placeholder
|
|
47
|
+
? { placeholder: plain(String(fp.placeholder)) }
|
|
48
|
+
: {}),
|
|
49
|
+
...(fp.initialValue
|
|
50
|
+
? { initial_value: String(fp.initialValue) }
|
|
51
|
+
: {}),
|
|
52
|
+
...(fp.maxLength ? { max_length: Number(fp.maxLength) } : {}),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
case "modal_select": {
|
|
56
|
+
const options = childOptions(node).map(optionFrom);
|
|
57
|
+
const init = options.find((o) => o.value === String(fp.initialOption ?? ""));
|
|
58
|
+
return {
|
|
59
|
+
type: "input",
|
|
60
|
+
block_id: id,
|
|
61
|
+
label,
|
|
62
|
+
optional,
|
|
63
|
+
element: {
|
|
64
|
+
type: "static_select",
|
|
65
|
+
action_id: id,
|
|
66
|
+
options,
|
|
67
|
+
...(fp.placeholder
|
|
68
|
+
? { placeholder: plain(String(fp.placeholder)) }
|
|
69
|
+
: {}),
|
|
70
|
+
...(init ? { initial_option: init } : {}),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
case "modal_radio": {
|
|
75
|
+
const options = childOptions(node).map(optionFrom);
|
|
76
|
+
const init = options.find((o) => o.value === String(fp.initialOption ?? ""));
|
|
77
|
+
return {
|
|
78
|
+
type: "input",
|
|
79
|
+
block_id: id,
|
|
80
|
+
label,
|
|
81
|
+
optional,
|
|
82
|
+
element: {
|
|
83
|
+
type: "radio_buttons",
|
|
84
|
+
action_id: id,
|
|
85
|
+
options,
|
|
86
|
+
...(init ? { initial_option: init } : {}),
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
default:
|
|
91
|
+
throw new ModalRenderError(`renderSlackModal: unsupported modal element "${String(node.type)}"`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
type: "modal",
|
|
96
|
+
callback_id: String(p.callbackId ?? ""),
|
|
97
|
+
title: plain(String(p.title ?? "")),
|
|
98
|
+
...(p.submitLabel
|
|
99
|
+
? { submit: plain(String(p.submitLabel)) }
|
|
100
|
+
: { submit: plain("Submit") }),
|
|
101
|
+
...(p.closeLabel ? { close: plain(String(p.closeLabel)) } : {}),
|
|
102
|
+
...(p.notifyOnClose ? { notify_on_close: true } : {}),
|
|
103
|
+
...(p.privateMetadata
|
|
104
|
+
? { private_metadata: String(p.privateMetadata) }
|
|
105
|
+
: {}),
|
|
106
|
+
blocks: blocks,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The credentialed Slack side-effects the run renderer performs, injected so
|
|
3
|
+
* {@link createRunRenderer} (event-renderer.ts) stays free of `@slack/web-api`
|
|
4
|
+
* (no `WebClient`, no Bolt). The native Slack adapter wraps a real `WebClient`;
|
|
5
|
+
* the managed Connector Outbox wraps its own credentialed sender — both drive
|
|
6
|
+
* the exact same renderer.
|
|
7
|
+
*
|
|
8
|
+
* These are the three ops the renderer calls directly. The four native
|
|
9
|
+
* streaming ops (`chat.startStream`/`appendStream`/`stopStream`) are injected
|
|
10
|
+
* separately as a {@link NativeStreamTransport} (native-stream.ts).
|
|
11
|
+
*/
|
|
12
|
+
export interface SlackRenderTransport {
|
|
13
|
+
/**
|
|
14
|
+
* `assistant.threads.setStatus` — the thread-anchored "is thinking…" /
|
|
15
|
+
* "is using `tool`…" indicator. Passing an empty `status` clears it.
|
|
16
|
+
*/
|
|
17
|
+
setStatus(args: {
|
|
18
|
+
channel_id: string;
|
|
19
|
+
thread_ts: string;
|
|
20
|
+
status: string;
|
|
21
|
+
loading_messages?: string[];
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* `chat.postMessage` — post a message (streaming placeholder, `:wrench:`
|
|
25
|
+
* tool-status row, or error notice). Resolves with the new message `ts`; the
|
|
26
|
+
* legacy text stream treats a missing `ts` as a hard failure.
|
|
27
|
+
*/
|
|
28
|
+
postMessage(args: {
|
|
29
|
+
channel: string;
|
|
30
|
+
thread_ts?: string;
|
|
31
|
+
text: string;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
ts?: string;
|
|
34
|
+
}>;
|
|
35
|
+
/** `chat.update` — edit an existing message in place. */
|
|
36
|
+
updateMessage(args: {
|
|
37
|
+
channel: string;
|
|
38
|
+
ts: string;
|
|
39
|
+
text: string;
|
|
40
|
+
}): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/render/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,SAAS,CAAC,IAAI,EAAE;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;KAC7B,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,IAAI,EAAE;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC;KACd,GAAG,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7B,yDAAyD;IACzD,aAAa,CAAC,IAAI,EAAE;QAClB,OAAO,EAAE,MAAM,CAAC;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;KACd,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@copilotkit/channels-slack/render` — the pure, Bolt-free Slack rendering surface.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here imports `@slack/bolt` or instantiates a `WebClient`: the
|
|
5
|
+
* stateful run renderer takes its credentialed side-effects as injected
|
|
6
|
+
* transports ({@link SlackRenderTransport} + {@link NativeStreamTransport}), and
|
|
7
|
+
* the IR→Block Kit / modal / mrkdwn helpers are pure functions. This is the
|
|
8
|
+
* seam the native Slack adapter AND the managed Connector Outbox both consume,
|
|
9
|
+
* so managed replies reach 1:1 UX parity with the native bot without forking
|
|
10
|
+
* the renderer.
|
|
11
|
+
*/
|
|
12
|
+
export { createRunRenderer } from "./event-renderer.js";
|
|
13
|
+
export type { SlackRenderTransport } from "./render/transport.js";
|
|
14
|
+
export { renderBlockKit, renderSlackMessage, buildFeedbackBlocks, FEEDBACK_ACTION_ID, } from "./render/block-kit.js";
|
|
15
|
+
export { renderSlackModal } from "./render/modal.js";
|
|
16
|
+
export { SLACK_LIMITS } from "./render/budget.js";
|
|
17
|
+
export { NativeMessageStream } from "./native-stream.js";
|
|
18
|
+
export { ChunkedMessageStream } from "./chunked-message-stream.js";
|
|
19
|
+
export { MessageStream } from "./message-stream.js";
|
|
20
|
+
export type { NativeMessageStreamConfig, NativeStreamTransport, TextStream, } from "./native-stream.js";
|
|
21
|
+
export { markdownToMrkdwn } from "./markdown-to-mrkdwn.js";
|
|
22
|
+
export { autoCloseOpenMarkdown } from "./auto-close-streaming.js";
|
|
23
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAElE,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EACV,yBAAyB,EACzB,qBAAqB,EACrB,UAAU,GACX,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC"}
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@copilotkit/channels-slack/render` — the pure, Bolt-free Slack rendering surface.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here imports `@slack/bolt` or instantiates a `WebClient`: the
|
|
5
|
+
* stateful run renderer takes its credentialed side-effects as injected
|
|
6
|
+
* transports ({@link SlackRenderTransport} + {@link NativeStreamTransport}), and
|
|
7
|
+
* the IR→Block Kit / modal / mrkdwn helpers are pure functions. This is the
|
|
8
|
+
* seam the native Slack adapter AND the managed Connector Outbox both consume,
|
|
9
|
+
* so managed replies reach 1:1 UX parity with the native bot without forking
|
|
10
|
+
* the renderer.
|
|
11
|
+
*/
|
|
12
|
+
export { createRunRenderer } from "./event-renderer.js";
|
|
13
|
+
export { renderBlockKit, renderSlackMessage, buildFeedbackBlocks, FEEDBACK_ACTION_ID, } from "./render/block-kit.js";
|
|
14
|
+
export { renderSlackModal } from "./render/modal.js";
|
|
15
|
+
export { SLACK_LIMITS } from "./render/budget.js";
|
|
16
|
+
export { NativeMessageStream } from "./native-stream.js";
|
|
17
|
+
export { ChunkedMessageStream } from "./chunked-message-stream.js";
|
|
18
|
+
export { MessageStream } from "./message-stream.js";
|
|
19
|
+
export { markdownToMrkdwn } from "./markdown-to-mrkdwn.js";
|
|
20
|
+
export { autoCloseOpenMarkdown } from "./auto-close-streaming.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { HttpAgent } from "@ag-ui/client";
|
|
2
|
+
import type { BaseEvent, RunAgentInput } from "@ag-ui/core";
|
|
3
|
+
import type { Observable } from "rxjs";
|
|
4
|
+
/**
|
|
5
|
+
* An `HttpAgent` that tolerates the AG-UI event streams real agents emit.
|
|
6
|
+
*
|
|
7
|
+
* `@ag-ui/client`'s stock transform re-validates every streamed event
|
|
8
|
+
* against a strict Zod schema. Some events that `@ag-ui/langgraph`
|
|
9
|
+
* legitimately emits fail it — notably a `TOOL_CALL_START` whose
|
|
10
|
+
* `parentMessageId` is `null` ("Expected string, received null") — and a
|
|
11
|
+
* single rejected event aborts the entire run. That breaks LangGraph
|
|
12
|
+
* interrupts / human-in-the-loop in Slack, where the tool call that
|
|
13
|
+
* triggers the interrupt carries exactly that shape.
|
|
14
|
+
*
|
|
15
|
+
* The bridge talks to a trusted runtime, so rather than re-validate its
|
|
16
|
+
* output we use the same SSE parse the stock path wraps
|
|
17
|
+
* (`parseSSEStream`) and coerce the known nullable-string fields. Note
|
|
18
|
+
* this deliberately drops the stock transform's *entire* strict Zod
|
|
19
|
+
* re-validation step (not just the offending field) — acceptable only
|
|
20
|
+
* because the runtime is trusted and `runHttpRequest` still throws on
|
|
21
|
+
* transport/HTTP errors. The first coercion logs a one-time breadcrumb so
|
|
22
|
+
* the workaround is visible in production. Revert to the stock transform
|
|
23
|
+
* (`transformHttpEventStream`) once upstream makes these fields nullable.
|
|
24
|
+
*/
|
|
25
|
+
export declare class SanitizingHttpAgent extends HttpAgent {
|
|
26
|
+
run(input: RunAgentInput): Observable<BaseEvent>;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=sanitizing-http-agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitizing-http-agent.d.ts","sourceRoot":"","sources":["../src/sanitizing-http-agent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAkC,MAAM,eAAe,CAAC;AAC1E,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,mBAAoB,SAAQ,SAAS;IAChD,GAAG,CAAC,KAAK,EAAE,aAAa,GAAG,UAAU,CAAC,SAAS,CAAC;CAMjD"}
|