@copilotkit/channels-teams 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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/dist/adapter.d.ts +133 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +539 -0
  6. package/dist/adapter.test.d.ts +2 -0
  7. package/dist/adapter.test.d.ts.map +1 -0
  8. package/dist/adapter.test.js +202 -0
  9. package/dist/conversation-store.d.ts +36 -0
  10. package/dist/conversation-store.d.ts.map +1 -0
  11. package/dist/conversation-store.js +78 -0
  12. package/dist/conversation-store.test.d.ts +2 -0
  13. package/dist/conversation-store.test.d.ts.map +1 -0
  14. package/dist/conversation-store.test.js +72 -0
  15. package/dist/download-files.d.ts +62 -0
  16. package/dist/download-files.d.ts.map +1 -0
  17. package/dist/download-files.js +190 -0
  18. package/dist/download-files.test.d.ts +2 -0
  19. package/dist/download-files.test.d.ts.map +1 -0
  20. package/dist/download-files.test.js +96 -0
  21. package/dist/event-renderer.d.ts +23 -0
  22. package/dist/event-renderer.d.ts.map +1 -0
  23. package/dist/event-renderer.js +135 -0
  24. package/dist/event-renderer.test.d.ts +2 -0
  25. package/dist/event-renderer.test.d.ts.map +1 -0
  26. package/dist/event-renderer.test.js +95 -0
  27. package/dist/graph-files.d.ts +50 -0
  28. package/dist/graph-files.d.ts.map +1 -0
  29. package/dist/graph-files.js +126 -0
  30. package/dist/graph-files.test.d.ts +2 -0
  31. package/dist/graph-files.test.d.ts.map +1 -0
  32. package/dist/graph-files.test.js +114 -0
  33. package/dist/index.d.ts +20 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +14 -0
  36. package/dist/interaction.d.ts +36 -0
  37. package/dist/interaction.d.ts.map +1 -0
  38. package/dist/interaction.js +35 -0
  39. package/dist/interaction.test.d.ts +2 -0
  40. package/dist/interaction.test.d.ts.map +1 -0
  41. package/dist/interaction.test.js +43 -0
  42. package/dist/listener.d.ts +26 -0
  43. package/dist/listener.d.ts.map +1 -0
  44. package/dist/listener.js +55 -0
  45. package/dist/listener.test.d.ts +2 -0
  46. package/dist/listener.test.d.ts.map +1 -0
  47. package/dist/listener.test.js +43 -0
  48. package/dist/message-stream.d.ts +52 -0
  49. package/dist/message-stream.d.ts.map +1 -0
  50. package/dist/message-stream.js +78 -0
  51. package/dist/message-stream.test.d.ts +2 -0
  52. package/dist/message-stream.test.d.ts.map +1 -0
  53. package/dist/message-stream.test.js +44 -0
  54. package/dist/render/adaptive-card.d.ts +43 -0
  55. package/dist/render/adaptive-card.d.ts.map +1 -0
  56. package/dist/render/adaptive-card.js +401 -0
  57. package/dist/render/adaptive-card.test.d.ts +2 -0
  58. package/dist/render/adaptive-card.test.d.ts.map +1 -0
  59. package/dist/render/adaptive-card.test.js +240 -0
  60. package/dist/render/auto-close.d.ts +27 -0
  61. package/dist/render/auto-close.d.ts.map +1 -0
  62. package/dist/render/auto-close.js +153 -0
  63. package/dist/render/auto-close.test.d.ts +2 -0
  64. package/dist/render/auto-close.test.d.ts.map +1 -0
  65. package/dist/render/auto-close.test.js +46 -0
  66. package/dist/render/budget.d.ts +38 -0
  67. package/dist/render/budget.d.ts.map +1 -0
  68. package/dist/render/budget.js +44 -0
  69. package/dist/render/budget.test.d.ts +2 -0
  70. package/dist/render/budget.test.d.ts.map +1 -0
  71. package/dist/render/budget.test.js +25 -0
  72. package/dist/render/markdown.d.ts +13 -0
  73. package/dist/render/markdown.d.ts.map +1 -0
  74. package/dist/render/markdown.js +121 -0
  75. package/dist/render/markdown.test.d.ts +2 -0
  76. package/dist/render/markdown.test.d.ts.map +1 -0
  77. package/dist/render/markdown.test.js +58 -0
  78. package/dist/sanitizing-http-agent.d.ts +35 -0
  79. package/dist/sanitizing-http-agent.d.ts.map +1 -0
  80. package/dist/sanitizing-http-agent.js +58 -0
  81. package/dist/types.d.ts +53 -0
  82. package/dist/types.d.ts.map +1 -0
  83. package/dist/types.js +1 -0
  84. package/package.json +67 -0
@@ -0,0 +1,401 @@
1
+ import { TEAMS_LIMITS, truncateText, clampArray } from "./budget.js";
2
+ /** Teams attachment content type for an Adaptive Card. */
3
+ export const ADAPTIVE_CARD_CONTENT_TYPE = "application/vnd.microsoft.card.adaptive";
4
+ const SCHEMA = "http://adaptivecards.io/schemas/adaptive-card.json";
5
+ const VERSION = "1.5";
6
+ /**
7
+ * Render a cross-platform component IR tree (already expanded by `renderToIR`
8
+ * and pre-bound by the action registry, so event props are `{ id }`) into a
9
+ * Teams **Adaptive Card** (1.5).
10
+ *
11
+ * Structural nodes map to body elements (`<Header>`→bold `TextBlock`,
12
+ * `<Section>`/`<Markdown>`→wrapped `TextBlock`, `<Fields>`→`FactSet`,
13
+ * `<Table>`→native `Table`, `<Image>`→`Image`). Interactive nodes split by
14
+ * Adaptive Card shape: `<Button>`→a top-level `Action.Submit` (per the V1
15
+ * decision to use `Action.Submit`), while `<Input>`/`<Select>` become
16
+ * `Input.Text`/`Input.ChoiceSet` in the body. Each action/input carries the
17
+ * registry-stamped opaque id in its `data`/`id` so a later interaction can be
18
+ * decoded back into the engine (round-trip is a follow-up; rendering is here).
19
+ *
20
+ * The renderer is total: unknown intrinsics are skipped. Collections clamp and
21
+ * text truncates to {@link TEAMS_LIMITS} so the card stays within Teams' payload
22
+ * ceiling.
23
+ */
24
+ export function renderAdaptiveCard(ir) {
25
+ const body = [];
26
+ const actions = [];
27
+ for (const node of ir)
28
+ renderNode(node, body, actions);
29
+ const card = {
30
+ type: "AdaptiveCard",
31
+ $schema: SCHEMA,
32
+ version: VERSION,
33
+ body: clampArray(body, TEAMS_LIMITS.bodyElements).items,
34
+ };
35
+ const clampedActions = clampArray(actions, TEAMS_LIMITS.actions).items;
36
+ if (clampedActions.length > 0)
37
+ card.actions = clampedActions;
38
+ return card;
39
+ }
40
+ /** Render a single IR node, pushing body elements and/or top-level actions. */
41
+ function renderNode(node, body, actions) {
42
+ if (typeof node.type !== "string")
43
+ return; // non-intrinsic, already expanded
44
+ const props = node.props ?? {};
45
+ switch (node.type) {
46
+ case "message":
47
+ // The message container is not an element; flatten its children.
48
+ for (const child of childNodes(node))
49
+ renderNode(child, body, actions);
50
+ return;
51
+ case "header":
52
+ body.push({
53
+ type: "TextBlock",
54
+ text: truncateText(collectText(node), TEAMS_LIMITS.textBlock),
55
+ size: "Large",
56
+ weight: "Bolder",
57
+ wrap: true,
58
+ });
59
+ return;
60
+ case "section":
61
+ case "markdown":
62
+ body.push(textBlock(collectText(node)));
63
+ return;
64
+ case "text":
65
+ body.push(textBlock(String(props.value ?? "")));
66
+ return;
67
+ case "context":
68
+ body.push({
69
+ type: "TextBlock",
70
+ text: truncateText(collectText(node), TEAMS_LIMITS.textBlock),
71
+ size: "Small",
72
+ isSubtle: true,
73
+ wrap: true,
74
+ });
75
+ return;
76
+ case "divider":
77
+ // Adaptive Cards has no rule element; a separator line is drawn *above*
78
+ // an element via `separator: true`. An empty, separated TextBlock reads
79
+ // as a horizontal divider.
80
+ body.push({
81
+ type: "TextBlock",
82
+ text: " ",
83
+ separator: true,
84
+ spacing: "Medium",
85
+ });
86
+ return;
87
+ case "image":
88
+ body.push({
89
+ type: "Image",
90
+ url: String(props.url ?? props.image_url ?? ""),
91
+ altText: String(props.alt ?? props.altText ?? ""),
92
+ size: "Auto",
93
+ });
94
+ return;
95
+ case "fields":
96
+ body.push(factSet(childNodes(node).filter((c) => c.type === "field")));
97
+ return;
98
+ case "field":
99
+ body.push(factSet([node]));
100
+ return;
101
+ case "table":
102
+ body.push(renderTable(node));
103
+ return;
104
+ case "chart":
105
+ body.push(renderChart(node));
106
+ return;
107
+ case "actions":
108
+ for (const child of childNodes(node))
109
+ renderNode(child, body, actions);
110
+ return;
111
+ case "button":
112
+ actions.push(renderButton(node));
113
+ return;
114
+ case "select":
115
+ body.push(renderSelect(node));
116
+ return;
117
+ case "input":
118
+ body.push(renderInput(node));
119
+ return;
120
+ default:
121
+ // Unknown intrinsic: skip (total renderer).
122
+ return;
123
+ }
124
+ }
125
+ function textBlock(text) {
126
+ return {
127
+ type: "TextBlock",
128
+ text: truncateText(text, TEAMS_LIMITS.textBlock),
129
+ wrap: true,
130
+ };
131
+ }
132
+ /** A `<Fields>`/`<Field>` group → a `FactSet`. Each field's text is split on
133
+ * its first colon into title/value (falling back to a value-only fact). */
134
+ function factSet(fieldNodes) {
135
+ const { items } = clampArray(fieldNodes, TEAMS_LIMITS.factsPerSet);
136
+ const facts = items.map((f) => {
137
+ const text = collectText(f);
138
+ const idx = text.indexOf(":");
139
+ if (idx > 0 && idx <= 60) {
140
+ return {
141
+ title: truncateText(text.slice(0, idx).trim(), TEAMS_LIMITS.factTitle),
142
+ value: truncateText(text.slice(idx + 1).trim(), TEAMS_LIMITS.factValue),
143
+ };
144
+ }
145
+ return { title: "", value: truncateText(text, TEAMS_LIMITS.factValue) };
146
+ });
147
+ return { type: "FactSet", facts };
148
+ }
149
+ function renderButton(node) {
150
+ const props = node.props ?? {};
151
+ // Link button → Action.OpenUrl (opens the URL; carries no submit data).
152
+ if (typeof props.url === "string" && props.url.length > 0) {
153
+ return {
154
+ type: "Action.OpenUrl",
155
+ title: truncateText(collectText(node), TEAMS_LIMITS.buttonText),
156
+ url: props.url,
157
+ };
158
+ }
159
+ const action = {
160
+ type: "Action.Submit",
161
+ title: truncateText(collectText(node), TEAMS_LIMITS.buttonText),
162
+ };
163
+ // Forward-ready: carry the opaque action id + value so a later
164
+ // `decodeInteraction` can route the submit back into the engine.
165
+ const id = idFromHandler(props.onClick);
166
+ const data = {};
167
+ if (id)
168
+ data.ckActionId = id;
169
+ if (props.value !== undefined)
170
+ data.value = props.value;
171
+ if (Object.keys(data).length > 0)
172
+ action.data = data;
173
+ if (props.style === "danger" || props.style === "destructive") {
174
+ action.style = "destructive";
175
+ }
176
+ else if (props.style === "primary") {
177
+ action.style = "positive";
178
+ }
179
+ return action;
180
+ }
181
+ function renderSelect(node) {
182
+ const props = node.props ?? {};
183
+ const options = props.options ?? [];
184
+ const { items } = clampArray(options, TEAMS_LIMITS.choices);
185
+ const el = {
186
+ type: "Input.ChoiceSet",
187
+ id: idFromHandler(props.onSelect) ?? "select",
188
+ choices: items.map((o) => ({
189
+ title: truncateText(String(o.label), TEAMS_LIMITS.choiceLabel),
190
+ value: String(o.value),
191
+ })),
192
+ };
193
+ // Multi-select: Teams submits the chosen values as a comma-joined string.
194
+ if (props.multi)
195
+ el.isMultiSelect = true;
196
+ if (props.placeholder)
197
+ el.placeholder = String(props.placeholder);
198
+ return el;
199
+ }
200
+ function renderInput(node) {
201
+ const props = node.props ?? {};
202
+ const el = {
203
+ type: "Input.Text",
204
+ id: idFromHandler(props.onSubmit) ?? "input",
205
+ };
206
+ if (props.placeholder)
207
+ el.placeholder = String(props.placeholder);
208
+ if (props.multiline)
209
+ el.isMultiline = true;
210
+ return el;
211
+ }
212
+ /** A `<Table>` → a native Adaptive Cards `Table` (1.5). */
213
+ function renderTable(node) {
214
+ const props = node.props ?? {};
215
+ const cell = (text, header = false) => ({
216
+ type: "TableCell",
217
+ items: [
218
+ {
219
+ type: "TextBlock",
220
+ text: truncateText(text, TEAMS_LIMITS.cellText),
221
+ wrap: true,
222
+ ...(header ? { weight: "Bolder" } : {}),
223
+ },
224
+ ],
225
+ });
226
+ const columnsProp = props.columns;
227
+ const columns = columnsProp
228
+ ? clampArray(columnsProp, TEAMS_LIMITS.tableColumns).items
229
+ : undefined;
230
+ const rows = [];
231
+ if (columns && columns.length > 0) {
232
+ rows.push({
233
+ type: "TableRow",
234
+ cells: columns.map((c) => cell(c.header, true)),
235
+ });
236
+ }
237
+ const rowNodes = childNodes(node).filter((c) => c.type === "row");
238
+ const { items: dataRows } = clampArray(rowNodes, TEAMS_LIMITS.tableRows);
239
+ for (const rowNode of dataRows) {
240
+ const cells = childNodes(rowNode).filter((c) => c.type === "cell");
241
+ rows.push({
242
+ type: "TableRow",
243
+ cells: cells.map((c) => cell(collectText(c))),
244
+ });
245
+ }
246
+ const table = {
247
+ type: "Table",
248
+ columns: (columns ?? inferColumns(rowNodes)).map((c) => ({
249
+ width: 1,
250
+ ...(typeof c === "object" && "align" in c && c.align
251
+ ? { horizontalCellContentAlignment: capitalize(c.align) }
252
+ : {}),
253
+ })),
254
+ rows,
255
+ firstRowAsHeader: !!(columns && columns.length > 0),
256
+ gridStyle: "default",
257
+ };
258
+ return table;
259
+ }
260
+ /**
261
+ * A `<Chart>` → a native Teams chart element (`Chart.VerticalBar` /
262
+ * `Chart.HorizontalBar` / `Chart.Line` / `Chart.Pie` / `Chart.Donut`). These
263
+ * are a Teams host extension: they render in Teams clients whose app manifest
264
+ * opts into chart support; other Adaptive Card hosts ignore the unknown
265
+ * element. Data points clamp and labels/title truncate to the budget.
266
+ */
267
+ function renderChart(node) {
268
+ const props = node.props ?? {};
269
+ const type = String(props.type ?? "verticalBar");
270
+ const title = props.title != null && String(props.title).length > 0
271
+ ? truncateText(String(props.title), TEAMS_LIMITS.chartTitle)
272
+ : undefined;
273
+ const rawData = Array.isArray(props.data)
274
+ ? props.data
275
+ : [];
276
+ const { items } = clampArray(rawData, TEAMS_LIMITS.chartDataPoints);
277
+ const points = items.map((p) => ({
278
+ label: truncateText(String(p?.label ?? ""), TEAMS_LIMITS.chartLabel),
279
+ value: Number.isFinite(Number(p?.value)) ? Number(p?.value) : 0,
280
+ }));
281
+ // Fields shared by every chart kind. `showTitle` is meaningless without a
282
+ // title; `maxWidth` keeps the chart from stretching the whole card.
283
+ const common = { maxWidth: "520px" };
284
+ if (title !== undefined) {
285
+ common.title = title;
286
+ common.showTitle = true;
287
+ }
288
+ // Axis titles apply to the cartesian charts (bar/line), not pie/donut.
289
+ const withAxes = (el) => {
290
+ if (props.xAxisTitle != null)
291
+ el.xAxisTitle = String(props.xAxisTitle);
292
+ if (props.yAxisTitle != null)
293
+ el.yAxisTitle = String(props.yAxisTitle);
294
+ return el;
295
+ };
296
+ const xy = points.map((p) => ({ x: p.label, y: p.value }));
297
+ const slices = points.map((p) => ({ legend: p.label, value: p.value }));
298
+ switch (type) {
299
+ case "horizontalBar":
300
+ return withAxes({ ...common, type: "Chart.HorizontalBar", data: xy });
301
+ case "line":
302
+ return withAxes({
303
+ ...common,
304
+ type: "Chart.Line",
305
+ data: [{ legend: title ?? "", values: xy }],
306
+ });
307
+ case "pie":
308
+ return { ...common, type: "Chart.Pie", data: slices };
309
+ case "donut":
310
+ return { ...common, type: "Chart.Donut", data: slices };
311
+ default:
312
+ // verticalBar — also the fallback for any unrecognized type.
313
+ return withAxes({
314
+ ...common,
315
+ type: "Chart.VerticalBar",
316
+ showBarValues: true,
317
+ data: xy,
318
+ });
319
+ }
320
+ }
321
+ /** When no explicit `columns` are given, size the grid to the widest row. */
322
+ function inferColumns(rowNodes) {
323
+ let widest = 0;
324
+ for (const r of rowNodes) {
325
+ const n = childNodes(r).filter((c) => c.type === "cell").length;
326
+ if (n > widest)
327
+ widest = n;
328
+ }
329
+ return Array.from({ length: Math.min(widest, TEAMS_LIMITS.tableColumns) }, () => ({}));
330
+ }
331
+ function capitalize(s) {
332
+ return s.charAt(0).toUpperCase() + s.slice(1);
333
+ }
334
+ /** Extract `{ id }` stamped onto an event prop by the action registry, if present. */
335
+ function idFromHandler(handler) {
336
+ if (handler && typeof handler === "object" && "id" in handler) {
337
+ const id = handler.id;
338
+ if (typeof id === "string")
339
+ return id;
340
+ }
341
+ return undefined;
342
+ }
343
+ /** The expanded `children` of an IR node as a `BotNode[]` (empty if none). */
344
+ function childNodes(node) {
345
+ const children = node.props?.children;
346
+ if (Array.isArray(children))
347
+ return children;
348
+ if (children &&
349
+ typeof children === "object" &&
350
+ "type" in children) {
351
+ return [children];
352
+ }
353
+ return [];
354
+ }
355
+ /** Concatenate the `value` of all descendant `text` nodes (depth-first). */
356
+ function collectText(node) {
357
+ if (typeof node.type === "string" && node.type === "text") {
358
+ return String(node.props?.value ?? "");
359
+ }
360
+ let acc = "";
361
+ for (const child of childNodes(node))
362
+ acc += collectText(child);
363
+ return acc;
364
+ }
365
+ /**
366
+ * Does this IR collapse to plain text (no structural or interactive elements)?
367
+ * Such replies are sent as a normal Teams text activity rather than wrapped in
368
+ * an Adaptive Card. A bare `Echo: hi` shouldn't render as a card.
369
+ */
370
+ export function isPlainText(ir) {
371
+ const RICH = new Set([
372
+ "header",
373
+ "fields",
374
+ "field",
375
+ "table",
376
+ "row",
377
+ "cell",
378
+ "chart",
379
+ "image",
380
+ "actions",
381
+ "button",
382
+ "select",
383
+ "input",
384
+ "divider",
385
+ "context",
386
+ ]);
387
+ const visit = (node) => {
388
+ if (typeof node.type === "string" && RICH.has(node.type))
389
+ return false;
390
+ return childNodes(node).every(visit);
391
+ };
392
+ return ir.every(visit);
393
+ }
394
+ /** Plain-text projection of an IR tree (depth-first text, blocks joined). */
395
+ export function collectPlainText(ir) {
396
+ return ir
397
+ .map((n) => collectText(n))
398
+ .filter((s) => s.length > 0)
399
+ .join("\n\n")
400
+ .trim();
401
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=adaptive-card.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adaptive-card.test.d.ts","sourceRoot":"","sources":["../../src/render/adaptive-card.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,240 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { renderAdaptiveCard, isPlainText, collectPlainText, } from "./adaptive-card.js";
3
+ const text = (value) => ({ type: "text", props: { value } });
4
+ const el = (type, children, props = {}) => ({
5
+ type,
6
+ props: { ...props, children },
7
+ });
8
+ const chart = (props) => ({
9
+ type: "chart",
10
+ props,
11
+ });
12
+ describe("renderAdaptiveCard", () => {
13
+ it("emits a versioned AdaptiveCard envelope", () => {
14
+ const card = renderAdaptiveCard([text("hi")]);
15
+ expect(card.type).toBe("AdaptiveCard");
16
+ expect(card.version).toBe("1.5");
17
+ expect(Array.isArray(card.body)).toBe(true);
18
+ });
19
+ it("renders a header as a bold large TextBlock", () => {
20
+ const card = renderAdaptiveCard([el("header", [text("Title")])]);
21
+ expect(card.body[0]).toMatchObject({
22
+ type: "TextBlock",
23
+ text: "Title",
24
+ weight: "Bolder",
25
+ size: "Large",
26
+ });
27
+ });
28
+ it("renders section/markdown as wrapped TextBlocks", () => {
29
+ const card = renderAdaptiveCard([el("section", [text("Body copy")])]);
30
+ expect(card.body[0]).toMatchObject({
31
+ type: "TextBlock",
32
+ text: "Body copy",
33
+ wrap: true,
34
+ });
35
+ });
36
+ it("renders <Fields> as a FactSet, splitting 'k: v' into title/value", () => {
37
+ const card = renderAdaptiveCard([
38
+ el("fields", [
39
+ el("field", [text("Status: Open")]),
40
+ el("field", [text("just a value")]),
41
+ ]),
42
+ ]);
43
+ expect(card.body[0]).toMatchObject({
44
+ type: "FactSet",
45
+ facts: [
46
+ { title: "Status", value: "Open" },
47
+ { title: "", value: "just a value" },
48
+ ],
49
+ });
50
+ });
51
+ it("renders a <Button> as a top-level Action.Submit carrying the opaque id", () => {
52
+ const card = renderAdaptiveCard([
53
+ el("actions", [
54
+ el("button", [text("Approve")], {
55
+ onClick: { id: "ck:approve" },
56
+ value: { decision: "yes" },
57
+ style: "primary",
58
+ }),
59
+ ]),
60
+ ]);
61
+ expect(card.body).toHaveLength(0);
62
+ expect(card.actions).toEqual([
63
+ {
64
+ type: "Action.Submit",
65
+ title: "Approve",
66
+ data: { ckActionId: "ck:approve", value: { decision: "yes" } },
67
+ style: "positive",
68
+ },
69
+ ]);
70
+ });
71
+ it("renders a url <Button> as an Action.OpenUrl", () => {
72
+ const card = renderAdaptiveCard([
73
+ el("actions", [
74
+ el("button", [text("Open")], { url: "https://dash/deploy/42" }),
75
+ ]),
76
+ ]);
77
+ expect(card.actions).toEqual([
78
+ { type: "Action.OpenUrl", title: "Open", url: "https://dash/deploy/42" },
79
+ ]);
80
+ });
81
+ it("marks a multi <Select> ChoiceSet as isMultiSelect", () => {
82
+ const card = renderAdaptiveCard([
83
+ el("select", [], {
84
+ multi: true,
85
+ onSelect: { id: "ck:pick" },
86
+ options: [{ label: "One", value: "1" }],
87
+ }),
88
+ ]);
89
+ expect(card.body[0]).toMatchObject({
90
+ type: "Input.ChoiceSet",
91
+ isMultiSelect: true,
92
+ });
93
+ });
94
+ it("renders <Select>/<Input> as body inputs", () => {
95
+ const card = renderAdaptiveCard([
96
+ el("select", [], {
97
+ onSelect: { id: "ck:pick" },
98
+ placeholder: "Choose",
99
+ options: [
100
+ { label: "One", value: "1" },
101
+ { label: "Two", value: "2" },
102
+ ],
103
+ }),
104
+ el("input", [], { onSubmit: { id: "ck:txt" }, multiline: true }),
105
+ ]);
106
+ expect(card.body[0]).toMatchObject({
107
+ type: "Input.ChoiceSet",
108
+ id: "ck:pick",
109
+ placeholder: "Choose",
110
+ choices: [
111
+ { title: "One", value: "1" },
112
+ { title: "Two", value: "2" },
113
+ ],
114
+ });
115
+ expect(card.body[1]).toMatchObject({
116
+ type: "Input.Text",
117
+ id: "ck:txt",
118
+ isMultiline: true,
119
+ });
120
+ });
121
+ it("renders a <Table> as a native Table with a header row", () => {
122
+ const card = renderAdaptiveCard([
123
+ el("table", [el("row", [el("cell", [text("a1")]), el("cell", [text("b1")])])], {
124
+ columns: [{ header: "A" }, { header: "B", align: "right" }],
125
+ }),
126
+ ]);
127
+ const table = card.body[0];
128
+ expect(table.type).toBe("Table");
129
+ expect(table.firstRowAsHeader).toBe(true);
130
+ const rows = table.rows;
131
+ expect(rows).toHaveLength(2); // header + 1 data row
132
+ expect(rows[0].type).toBe("TableRow");
133
+ });
134
+ it("clamps top-level actions to the Teams ceiling", () => {
135
+ const buttons = Array.from({ length: 10 }, (_, i) => el("button", [text(`b${i}`)], { onClick: { id: `ck:${i}` } }));
136
+ const card = renderAdaptiveCard([el("actions", buttons)]);
137
+ expect(card.actions).toHaveLength(6);
138
+ });
139
+ it("skips unknown intrinsics without throwing", () => {
140
+ const card = renderAdaptiveCard([el("mystery", [text("x")])]);
141
+ expect(card.body).toHaveLength(0);
142
+ });
143
+ it("renders a vertical-bar <Chart> with title and axis titles", () => {
144
+ const card = renderAdaptiveCard([
145
+ chart({
146
+ title: "Tickets",
147
+ xAxisTitle: "Month",
148
+ yAxisTitle: "Count",
149
+ data: [
150
+ { label: "Jan", value: 3 },
151
+ { label: "Feb", value: 7 },
152
+ ],
153
+ }),
154
+ ]);
155
+ expect(card.body[0]).toMatchObject({
156
+ type: "Chart.VerticalBar",
157
+ title: "Tickets",
158
+ showTitle: true,
159
+ showBarValues: true,
160
+ maxWidth: "520px",
161
+ xAxisTitle: "Month",
162
+ yAxisTitle: "Count",
163
+ data: [
164
+ { x: "Jan", y: 3 },
165
+ { x: "Feb", y: 7 },
166
+ ],
167
+ });
168
+ });
169
+ it("defaults an absent chart type to a vertical bar", () => {
170
+ const card = renderAdaptiveCard([
171
+ chart({ title: "T", data: [{ label: "a", value: 1 }] }),
172
+ ]);
173
+ expect(card.body[0].type).toBe("Chart.VerticalBar");
174
+ });
175
+ it("maps pie/donut to legend+value slices (no axes)", () => {
176
+ const data = [
177
+ { label: "Open", value: 4 },
178
+ { label: "Closed", value: 6 },
179
+ ];
180
+ const pie = renderAdaptiveCard([chart({ type: "pie", data })]);
181
+ expect(pie.body[0]).toMatchObject({
182
+ type: "Chart.Pie",
183
+ data: [
184
+ { legend: "Open", value: 4 },
185
+ { legend: "Closed", value: 6 },
186
+ ],
187
+ });
188
+ expect(pie.body[0].xAxisTitle).toBeUndefined();
189
+ const donut = renderAdaptiveCard([chart({ type: "donut", data })]);
190
+ expect(donut.body[0].type).toBe("Chart.Donut");
191
+ });
192
+ it("maps line to a single legended series of values", () => {
193
+ const card = renderAdaptiveCard([
194
+ chart({
195
+ type: "line",
196
+ title: "Revenue",
197
+ data: [{ label: "Q1", value: 100 }],
198
+ }),
199
+ ]);
200
+ expect(card.body[0]).toMatchObject({
201
+ type: "Chart.Line",
202
+ data: [{ legend: "Revenue", values: [{ x: "Q1", y: 100 }] }],
203
+ });
204
+ });
205
+ it("maps horizontalBar to an x/y series", () => {
206
+ const card = renderAdaptiveCard([
207
+ chart({ type: "horizontalBar", data: [{ label: "A", value: 5 }] }),
208
+ ]);
209
+ expect(card.body[0]).toMatchObject({
210
+ type: "Chart.HorizontalBar",
211
+ data: [{ x: "A", y: 5 }],
212
+ });
213
+ });
214
+ it("clamps chart data points to the Teams ceiling", () => {
215
+ const data = Array.from({ length: 80 }, (_, i) => ({
216
+ label: `p${i}`,
217
+ value: i,
218
+ }));
219
+ const card = renderAdaptiveCard([chart({ data })]);
220
+ const points = card.body[0].data;
221
+ expect(points).toHaveLength(50);
222
+ });
223
+ });
224
+ describe("isPlainText", () => {
225
+ it("is true for text-only trees", () => {
226
+ expect(isPlainText([text("hi")])).toBe(true);
227
+ expect(isPlainText([el("message", [el("section", [text("hi")])])])).toBe(true);
228
+ });
229
+ it("is false once any rich element appears", () => {
230
+ expect(isPlainText([el("header", [text("hi")])])).toBe(false);
231
+ expect(isPlainText([el("actions", [el("button", [text("x")])])])).toBe(false);
232
+ expect(isPlainText([el("message", [chart({ data: [] })])])).toBe(false);
233
+ });
234
+ });
235
+ describe("collectPlainText", () => {
236
+ it("joins block text depth-first", () => {
237
+ const ir = [el("message", [el("section", [text("a")]), text("b")])];
238
+ expect(collectPlainText(ir)).toBe("ab");
239
+ });
240
+ });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Streaming-polish layer for Teams' post-then-edit streaming.
3
+ *
4
+ * While an agent is mid-stream the buffer is usually an *unfinished* markdown
5
+ * document: an open code fence, an unclosed `**`, etc. Teams renders message
6
+ * text as markdown, so editing the message with unbalanced text makes the rest
7
+ * of the bubble render broken (a stray ``` turns the remainder into a code
8
+ * block until the closer arrives, a dangling `**` bolds the rest, and so on).
9
+ *
10
+ * `autoCloseOpenMarkdown` returns a balanced copy of the buffer by appending the
11
+ * minimum set of closers needed for well-formed display. When the agent later
12
+ * emits the real closer the buffer balances on its own and this adds nothing,
13
+ * so the *committed* (finalized) message has no synthetic closers.
14
+ *
15
+ * Handled, in priority order:
16
+ * 1. Unclosed fenced code blocks ``` (most severe: leaks code styling)
17
+ * 2. Unclosed inline code
18
+ * 3. Unclosed bold `**` / `__`, italic `*` / `_`, strike `~~`
19
+ *
20
+ * Heuristics:
21
+ * - A marker with no content after it (trailing `**`) is NOT closed. The
22
+ * agent may just be opening it; closing would flash a transient `****`.
23
+ * - Markers inside code/fence regions don't count.
24
+ * - Closers are emitted innermost-first so the structure nests correctly.
25
+ */
26
+ export declare function autoCloseOpenMarkdown(text: string): string;
27
+ //# sourceMappingURL=auto-close.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-close.d.ts","sourceRoot":"","sources":["../../src/render/auto-close.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAOH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkE1D"}