@chat-adapter/slack 4.17.0 → 4.19.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/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { AsyncLocalStorage } from "async_hooks";
3
3
  import { createHmac, timingSafeEqual } from "crypto";
4
4
  import {
5
5
  AdapterRateLimitError,
6
+ AuthenticationError,
6
7
  extractCard,
7
8
  extractFiles,
8
9
  NetworkError,
@@ -11,12 +12,12 @@ import {
11
12
  } from "@chat-adapter/shared";
12
13
  import { WebClient } from "@slack/web-api";
13
14
  import {
14
- ChatError,
15
15
  ConsoleLogger,
16
16
  convertEmojiPlaceholders,
17
17
  defaultEmojiResolver,
18
18
  isJSX,
19
19
  Message,
20
+ parseMarkdown as parseMarkdown2,
20
21
  StreamingMarkdownRenderer,
21
22
  toModalElement
22
23
  } from "chat";
@@ -59,13 +60,14 @@ function cardToBlockKit(card) {
59
60
  alt_text: card.title || "Card image"
60
61
  });
61
62
  }
63
+ const state = { usedNativeTable: false };
62
64
  for (const child of card.children) {
63
- const childBlocks = convertChildToBlocks(child);
65
+ const childBlocks = convertChildToBlocks(child, state);
64
66
  blocks.push(...childBlocks);
65
67
  }
66
68
  return blocks;
67
69
  }
68
- function convertChildToBlocks(child) {
70
+ function convertChildToBlocks(child, state) {
69
71
  switch (child.type) {
70
72
  case "text":
71
73
  return [convertTextToBlock(child)];
@@ -76,13 +78,13 @@ function convertChildToBlocks(child) {
76
78
  case "actions":
77
79
  return [convertActionsToBlock(child)];
78
80
  case "section":
79
- return convertSectionToBlocks(child);
81
+ return convertSectionToBlocks(child, state);
80
82
  case "fields":
81
83
  return [convertFieldsToBlock(child)];
82
84
  case "link":
83
85
  return [convertLinkToBlock(child)];
84
86
  case "table":
85
- return convertTableToBlocks(child);
87
+ return convertTableToBlocks(child, state);
86
88
  default: {
87
89
  const text = cardChildToFallbackText(child);
88
90
  if (text) {
@@ -250,10 +252,10 @@ function convertRadioSelectToElement(radioSelect) {
250
252
  }
251
253
  return element;
252
254
  }
253
- function convertTableToBlocks(element) {
255
+ function convertTableToBlocks(element, state) {
254
256
  const MAX_ROWS = 100;
255
257
  const MAX_COLS = 20;
256
- if (element.rows.length > MAX_ROWS || element.headers.length > MAX_COLS) {
258
+ if (state.usedNativeTable || element.rows.length > MAX_ROWS || element.headers.length > MAX_COLS) {
257
259
  return [
258
260
  {
259
261
  type: "section",
@@ -266,31 +268,28 @@ ${tableElementToAscii(element.headers, element.rows)}
266
268
  }
267
269
  ];
268
270
  }
269
- const columns = element.headers.map((header, i) => ({
270
- id: `col_${i}`,
271
- header: {
272
- type: "plain_text",
273
- text: convertEmoji(header)
274
- }
271
+ state.usedNativeTable = true;
272
+ const headerRow = element.headers.map((header) => ({
273
+ type: "raw_text",
274
+ text: convertEmoji(header)
275
275
  }));
276
- const rows = element.rows.map((row) => ({
277
- cells: row.map((cell) => ({
278
- type: "plain_text",
276
+ const dataRows = element.rows.map(
277
+ (row) => row.map((cell) => ({
278
+ type: "raw_text",
279
279
  text: convertEmoji(cell)
280
280
  }))
281
- }));
281
+ );
282
282
  return [
283
283
  {
284
284
  type: "table",
285
- columns,
286
- rows
285
+ rows: [headerRow, ...dataRows]
287
286
  }
288
287
  ];
289
288
  }
290
- function convertSectionToBlocks(element) {
289
+ function convertSectionToBlocks(element, state) {
291
290
  const blocks = [];
292
291
  for (const child of element.children) {
293
- blocks.push(...convertChildToBlocks(child));
292
+ blocks.push(...convertChildToBlocks(child, state));
294
293
  }
295
294
  return blocks;
296
295
  }
@@ -428,16 +427,68 @@ var SlackFormatConverter = class extends BaseFormatConverter {
428
427
  */
429
428
  toAst(mrkdwn) {
430
429
  let markdown = mrkdwn;
431
- markdown = markdown.replace(/<@([^|>]+)\|([^>]+)>/g, "@$2");
432
- markdown = markdown.replace(/<@([^>]+)>/g, "@$1");
433
- markdown = markdown.replace(/<#[^|>]+\|([^>]+)>/g, "#$1");
434
- markdown = markdown.replace(/<#([^>]+)>/g, "#$1");
430
+ markdown = markdown.replace(/<@([A-Z0-9_]+)\|([^>]+)>/g, "@$2");
431
+ markdown = markdown.replace(/<@([A-Z0-9_]+)>/g, "@$1");
432
+ markdown = markdown.replace(/<#[A-Z0-9_]+\|([^>]+)>/g, "#$1");
433
+ markdown = markdown.replace(/<#([A-Z0-9_]+)>/g, "#$1");
435
434
  markdown = markdown.replace(/<(https?:\/\/[^|>]+)\|([^>]+)>/g, "[$2]($1)");
436
435
  markdown = markdown.replace(/<(https?:\/\/[^>]+)>/g, "$1");
437
436
  markdown = markdown.replace(/(?<![_*\\])\*([^*\n]+)\*(?![_*])/g, "**$1**");
438
437
  markdown = markdown.replace(/(?<!~)~([^~\n]+)~(?!~)/g, "~~$1~~");
439
438
  return parseMarkdown(markdown);
440
439
  }
440
+ /**
441
+ * Convert AST to Slack blocks, using a native table block for the first table.
442
+ * Returns null if the AST contains no tables (caller should use regular text).
443
+ * Slack allows at most one table block per message; additional tables use ASCII.
444
+ */
445
+ toBlocksWithTable(ast) {
446
+ const hasTable = ast.children.some((node) => isTableNode(node));
447
+ if (!hasTable) {
448
+ return null;
449
+ }
450
+ const blocks = [];
451
+ let usedNativeTable = false;
452
+ let textBuffer = [];
453
+ const flushText = () => {
454
+ if (textBuffer.length > 0) {
455
+ const text = textBuffer.join("\n\n");
456
+ if (text.trim()) {
457
+ blocks.push({
458
+ type: "section",
459
+ text: { type: "mrkdwn", text }
460
+ });
461
+ }
462
+ textBuffer = [];
463
+ }
464
+ };
465
+ for (const child of ast.children) {
466
+ const node = child;
467
+ if (isTableNode(node)) {
468
+ flushText();
469
+ if (usedNativeTable) {
470
+ blocks.push({
471
+ type: "section",
472
+ text: {
473
+ type: "mrkdwn",
474
+ text: `\`\`\`
475
+ ${tableToAscii(node)}
476
+ \`\`\``
477
+ }
478
+ });
479
+ } else {
480
+ blocks.push(
481
+ mdastTableToSlackBlock(node, this.nodeToMrkdwn.bind(this))
482
+ );
483
+ usedNativeTable = true;
484
+ }
485
+ } else {
486
+ textBuffer.push(this.nodeToMrkdwn(node));
487
+ }
488
+ }
489
+ flushText();
490
+ return blocks;
491
+ }
441
492
  nodeToMrkdwn(node) {
442
493
  if (isParagraphNode(node)) {
443
494
  return getNodeChildren(node).map((child) => this.nodeToMrkdwn(child)).join("");
@@ -489,6 +540,26 @@ ${tableToAscii(node)}
489
540
  return this.defaultNodeToText(node, (child) => this.nodeToMrkdwn(child));
490
541
  }
491
542
  };
543
+ function mdastTableToSlackBlock(node, cellConverter) {
544
+ const rows = [];
545
+ for (const row of node.children) {
546
+ const cells = getNodeChildren(row).map((cell) => ({
547
+ type: "raw_text",
548
+ text: getNodeChildren(cell).map(cellConverter).join("")
549
+ }));
550
+ rows.push(cells);
551
+ }
552
+ const block = { type: "table", rows };
553
+ if (node.align) {
554
+ const columnSettings = node.align.map(
555
+ (a) => ({
556
+ align: a || "left"
557
+ })
558
+ );
559
+ block.column_settings = columnSettings;
560
+ }
561
+ return block;
562
+ }
492
563
 
493
564
  // src/modals.ts
494
565
  function encodeModalMetadata(meta) {
@@ -636,6 +707,7 @@ function radioSelectToBlock(radioSelect) {
636
707
  }
637
708
 
638
709
  // src/index.ts
710
+ var SLACK_USER_ID_PATTERN = /^[A-Z0-9_]+$/;
639
711
  var SlackAdapter = class _SlackAdapter {
640
712
  name = "slack";
641
713
  userName;
@@ -700,9 +772,9 @@ var SlackAdapter = class _SlackAdapter {
700
772
  if (this.defaultBotToken) {
701
773
  return this.defaultBotToken;
702
774
  }
703
- throw new ChatError(
704
- "No bot token available. In multi-workspace mode, ensure the webhook is being processed.",
705
- "MISSING_BOT_TOKEN"
775
+ throw new AuthenticationError(
776
+ "slack",
777
+ "No bot token available. In multi-workspace mode, ensure the webhook is being processed."
706
778
  );
707
779
  }
708
780
  /**
@@ -747,9 +819,9 @@ var SlackAdapter = class _SlackAdapter {
747
819
  */
748
820
  async setInstallation(teamId, installation) {
749
821
  if (!this.chat) {
750
- throw new ChatError(
751
- "Adapter not initialized. Ensure chat.initialize() has been called first.",
752
- "NOT_INITIALIZED"
822
+ throw new ValidationError(
823
+ "slack",
824
+ "Adapter not initialized. Ensure chat.initialize() has been called first."
753
825
  );
754
826
  }
755
827
  const state = this.chat.getState();
@@ -769,9 +841,9 @@ var SlackAdapter = class _SlackAdapter {
769
841
  */
770
842
  async getInstallation(teamId) {
771
843
  if (!this.chat) {
772
- throw new ChatError(
773
- "Adapter not initialized. Ensure chat.initialize() has been called first.",
774
- "NOT_INITIALIZED"
844
+ throw new ValidationError(
845
+ "slack",
846
+ "Adapter not initialized. Ensure chat.initialize() has been called first."
775
847
  );
776
848
  }
777
849
  const state = this.chat.getState();
@@ -798,17 +870,17 @@ var SlackAdapter = class _SlackAdapter {
798
870
  */
799
871
  async handleOAuthCallback(request) {
800
872
  if (!(this.clientId && this.clientSecret)) {
801
- throw new ChatError(
802
- "clientId and clientSecret are required for OAuth. Pass them in createSlackAdapter().",
803
- "MISSING_OAUTH_CONFIG"
873
+ throw new ValidationError(
874
+ "slack",
875
+ "clientId and clientSecret are required for OAuth. Pass them in createSlackAdapter()."
804
876
  );
805
877
  }
806
878
  const url = new URL(request.url);
807
879
  const code = url.searchParams.get("code");
808
880
  if (!code) {
809
- throw new ChatError(
810
- "Missing 'code' query parameter in OAuth callback request.",
811
- "MISSING_OAUTH_CODE"
881
+ throw new ValidationError(
882
+ "slack",
883
+ "Missing 'code' query parameter in OAuth callback request."
812
884
  );
813
885
  }
814
886
  const redirectUri = url.searchParams.get("redirect_uri") ?? void 0;
@@ -819,9 +891,9 @@ var SlackAdapter = class _SlackAdapter {
819
891
  redirect_uri: redirectUri
820
892
  });
821
893
  if (!(result.ok && result.access_token && result.team?.id)) {
822
- throw new ChatError(
823
- `Slack OAuth failed: ${result.error || "missing access_token or team.id"}`,
824
- "OAUTH_FAILED"
894
+ throw new AuthenticationError(
895
+ "slack",
896
+ `Slack OAuth failed: ${result.error || "missing access_token or team.id"}`
825
897
  );
826
898
  }
827
899
  const teamId = result.team.id;
@@ -838,9 +910,9 @@ var SlackAdapter = class _SlackAdapter {
838
910
  */
839
911
  async deleteInstallation(teamId) {
840
912
  if (!this.chat) {
841
- throw new ChatError(
842
- "Adapter not initialized. Ensure chat.initialize() has been called first.",
843
- "NOT_INITIALIZED"
913
+ throw new ValidationError(
914
+ "slack",
915
+ "Adapter not initialized. Ensure chat.initialize() has been called first."
844
916
  );
845
917
  }
846
918
  const state = this.chat.getState();
@@ -1257,7 +1329,10 @@ var SlackAdapter = class _SlackAdapter {
1257
1329
  if (isJSX(modal)) {
1258
1330
  const converted = toModalElement(modal);
1259
1331
  if (!converted) {
1260
- throw new Error("Invalid JSX element: must be a Modal element");
1332
+ throw new ValidationError(
1333
+ "slack",
1334
+ "Invalid JSX element: must be a Modal element"
1335
+ );
1261
1336
  }
1262
1337
  return converted;
1263
1338
  }
@@ -1344,7 +1419,7 @@ var SlackAdapter = class _SlackAdapter {
1344
1419
  /**
1345
1420
  * Handle reaction events from Slack (reaction_added, reaction_removed).
1346
1421
  */
1347
- handleReactionEvent(event, options) {
1422
+ async handleReactionEvent(event, options) {
1348
1423
  if (!this.chat) {
1349
1424
  this.logger.warn("Chat instance not initialized, ignoring reaction");
1350
1425
  return;
@@ -1355,9 +1430,32 @@ var SlackAdapter = class _SlackAdapter {
1355
1430
  });
1356
1431
  return;
1357
1432
  }
1433
+ let parentTs = event.item.ts;
1434
+ try {
1435
+ const result = await this.client.conversations.replies(
1436
+ this.withToken({
1437
+ channel: event.item.channel,
1438
+ ts: event.item.ts,
1439
+ limit: 1
1440
+ })
1441
+ );
1442
+ const firstMessage = result.messages?.[0];
1443
+ if (firstMessage?.thread_ts) {
1444
+ parentTs = firstMessage.thread_ts;
1445
+ }
1446
+ } catch (error) {
1447
+ this.logger.warn(
1448
+ "Failed to resolve parent thread for reaction, using message ts",
1449
+ {
1450
+ error: String(error),
1451
+ channel: event.item.channel,
1452
+ ts: event.item.ts
1453
+ }
1454
+ );
1455
+ }
1358
1456
  const threadId = this.encodeThreadId({
1359
1457
  channel: event.item.channel,
1360
- threadTs: event.item.ts
1458
+ threadTs: parentTs
1361
1459
  });
1362
1460
  const messageId = event.item.ts;
1363
1461
  const rawEmoji = event.reaction;
@@ -1569,12 +1667,22 @@ var SlackAdapter = class _SlackAdapter {
1569
1667
  * detection doesn't apply.
1570
1668
  */
1571
1669
  async resolveInlineMentions(text, skipSelfMention) {
1572
- const mentionPattern = /<@([A-Z0-9]+)(?:\|[^>]*)?>/g;
1573
1670
  const userIds = /* @__PURE__ */ new Set();
1574
- let match = mentionPattern.exec(text);
1575
- while (match) {
1576
- userIds.add(match[1]);
1577
- match = mentionPattern.exec(text);
1671
+ for (const segment of text.split("<")) {
1672
+ const end = segment.indexOf(">");
1673
+ if (end === -1) {
1674
+ continue;
1675
+ }
1676
+ const inner = segment.slice(0, end);
1677
+ if (!inner.startsWith("@")) {
1678
+ continue;
1679
+ }
1680
+ const rest = inner.slice(1);
1681
+ const pipeIdx = rest.indexOf("|");
1682
+ const uid = pipeIdx >= 0 ? rest.slice(0, pipeIdx) : rest;
1683
+ if (SLACK_USER_ID_PATTERN.test(uid)) {
1684
+ userIds.add(uid);
1685
+ }
1578
1686
  }
1579
1687
  if (userIds.size === 0) {
1580
1688
  return text;
@@ -1592,10 +1700,29 @@ var SlackAdapter = class _SlackAdapter {
1592
1700
  })
1593
1701
  );
1594
1702
  const nameMap = new Map(lookups);
1595
- return text.replace(/<@([A-Z0-9]+)(?:\|[^>]*)?>/g, (_m, uid) => {
1596
- const name = nameMap.get(uid);
1597
- return name ? `<@${uid}|${name}>` : `<@${uid}>`;
1598
- });
1703
+ let result = "";
1704
+ let remaining = text;
1705
+ let startIdx = remaining.indexOf("<@");
1706
+ while (startIdx !== -1) {
1707
+ result += remaining.slice(0, startIdx);
1708
+ remaining = remaining.slice(startIdx);
1709
+ const endIdx = remaining.indexOf(">");
1710
+ if (endIdx === -1) {
1711
+ break;
1712
+ }
1713
+ const inner = remaining.slice(2, endIdx);
1714
+ const pipeIdx = inner.indexOf("|");
1715
+ const uid = pipeIdx >= 0 ? inner.slice(0, pipeIdx) : inner;
1716
+ if (SLACK_USER_ID_PATTERN.test(uid)) {
1717
+ const name = nameMap.get(uid);
1718
+ result += name ? `<@${uid}|${name}>` : `<@${uid}>`;
1719
+ } else {
1720
+ result += remaining.slice(0, endIdx + 1);
1721
+ }
1722
+ remaining = remaining.slice(endIdx + 1);
1723
+ startIdx = remaining.indexOf("<@");
1724
+ }
1725
+ return result + remaining;
1599
1726
  }
1600
1727
  async parseSlackMessage(event, threadId, options) {
1601
1728
  const isMe = this.isMessageFromSelf(event);
@@ -1672,6 +1799,32 @@ var SlackAdapter = class _SlackAdapter {
1672
1799
  } : void 0
1673
1800
  };
1674
1801
  }
1802
+ /**
1803
+ * Try to render a message using native Slack table blocks.
1804
+ * Returns blocks + fallback text if the message contains tables, null otherwise.
1805
+ */
1806
+ renderWithTableBlocks(message) {
1807
+ let ast = null;
1808
+ if (typeof message === "object" && message !== null) {
1809
+ if ("ast" in message) {
1810
+ ast = message.ast;
1811
+ } else if ("markdown" in message) {
1812
+ ast = parseMarkdown2(message.markdown);
1813
+ }
1814
+ }
1815
+ if (!ast) {
1816
+ return null;
1817
+ }
1818
+ const blocks = this.formatConverter.toBlocksWithTable(ast);
1819
+ if (!blocks) {
1820
+ return null;
1821
+ }
1822
+ const fallbackText = convertEmojiPlaceholders(
1823
+ this.formatConverter.renderPostable(message),
1824
+ "slack"
1825
+ );
1826
+ return { text: fallbackText, blocks };
1827
+ }
1675
1828
  async postMessage(threadId, message) {
1676
1829
  const { channel, threadTs } = this.decodeThreadId(threadId);
1677
1830
  try {
@@ -1718,6 +1871,33 @@ var SlackAdapter = class _SlackAdapter {
1718
1871
  raw: result2
1719
1872
  };
1720
1873
  }
1874
+ const tableResult = this.renderWithTableBlocks(message);
1875
+ if (tableResult) {
1876
+ this.logger.debug("Slack API: chat.postMessage (table blocks)", {
1877
+ channel,
1878
+ threadTs,
1879
+ blockCount: tableResult.blocks.length
1880
+ });
1881
+ const result2 = await this.client.chat.postMessage(
1882
+ this.withToken({
1883
+ channel,
1884
+ thread_ts: threadTs,
1885
+ text: tableResult.text,
1886
+ blocks: tableResult.blocks,
1887
+ unfurl_links: false,
1888
+ unfurl_media: false
1889
+ })
1890
+ );
1891
+ this.logger.debug("Slack API: chat.postMessage response", {
1892
+ messageId: result2.ts,
1893
+ ok: result2.ok
1894
+ });
1895
+ return {
1896
+ id: result2.ts,
1897
+ threadId,
1898
+ raw: result2
1899
+ };
1900
+ }
1721
1901
  const text = convertEmojiPlaceholders(
1722
1902
  this.formatConverter.renderPostable(message),
1723
1903
  "slack"
@@ -1782,6 +1962,34 @@ var SlackAdapter = class _SlackAdapter {
1782
1962
  raw: result2
1783
1963
  };
1784
1964
  }
1965
+ const tableResult = this.renderWithTableBlocks(message);
1966
+ if (tableResult) {
1967
+ this.logger.debug("Slack API: chat.postEphemeral (table blocks)", {
1968
+ channel,
1969
+ threadTs,
1970
+ userId,
1971
+ blockCount: tableResult.blocks.length
1972
+ });
1973
+ const result2 = await this.client.chat.postEphemeral(
1974
+ this.withToken({
1975
+ channel,
1976
+ thread_ts: threadTs || void 0,
1977
+ user: userId,
1978
+ text: tableResult.text,
1979
+ blocks: tableResult.blocks
1980
+ })
1981
+ );
1982
+ this.logger.debug("Slack API: chat.postEphemeral response", {
1983
+ messageTs: result2.message_ts,
1984
+ ok: result2.ok
1985
+ });
1986
+ return {
1987
+ id: result2.message_ts || "",
1988
+ threadId,
1989
+ usedFallback: false,
1990
+ raw: result2
1991
+ };
1992
+ }
1785
1993
  const text = convertEmojiPlaceholders(
1786
1994
  this.formatConverter.renderPostable(message),
1787
1995
  "slack"
@@ -1814,6 +2022,95 @@ var SlackAdapter = class _SlackAdapter {
1814
2022
  this.handleSlackError(error);
1815
2023
  }
1816
2024
  }
2025
+ async scheduleMessage(threadId, message, options) {
2026
+ const { channel, threadTs } = this.decodeThreadId(threadId);
2027
+ const postAtUnix = Math.floor(options.postAt.getTime() / 1e3);
2028
+ if (postAtUnix <= Math.floor(Date.now() / 1e3)) {
2029
+ throw new ValidationError("slack", "postAt must be in the future");
2030
+ }
2031
+ const files = extractFiles(message);
2032
+ if (files.length > 0) {
2033
+ throw new ValidationError(
2034
+ "slack",
2035
+ "File uploads are not supported in scheduled messages"
2036
+ );
2037
+ }
2038
+ const token = this.getToken();
2039
+ try {
2040
+ const card = extractCard(message);
2041
+ if (card) {
2042
+ const blocks = cardToBlockKit(card);
2043
+ const fallbackText = cardToFallbackText(card);
2044
+ this.logger.debug("Slack API: chat.scheduleMessage (blocks)", {
2045
+ channel,
2046
+ threadTs,
2047
+ postAt: postAtUnix,
2048
+ blockCount: blocks.length
2049
+ });
2050
+ const result2 = await this.client.chat.scheduleMessage({
2051
+ token,
2052
+ channel,
2053
+ thread_ts: threadTs || void 0,
2054
+ post_at: postAtUnix,
2055
+ text: fallbackText,
2056
+ blocks,
2057
+ unfurl_links: false,
2058
+ unfurl_media: false
2059
+ });
2060
+ const scheduledMessageId2 = result2.scheduled_message_id;
2061
+ const adapter2 = this;
2062
+ return {
2063
+ scheduledMessageId: scheduledMessageId2,
2064
+ channelId: channel,
2065
+ postAt: options.postAt,
2066
+ raw: result2,
2067
+ async cancel() {
2068
+ await adapter2.client.chat.deleteScheduledMessage({
2069
+ token,
2070
+ channel,
2071
+ scheduled_message_id: scheduledMessageId2
2072
+ });
2073
+ }
2074
+ };
2075
+ }
2076
+ const text = convertEmojiPlaceholders(
2077
+ this.formatConverter.renderPostable(message),
2078
+ "slack"
2079
+ );
2080
+ this.logger.debug("Slack API: chat.scheduleMessage", {
2081
+ channel,
2082
+ threadTs,
2083
+ postAt: postAtUnix,
2084
+ textLength: text.length
2085
+ });
2086
+ const result = await this.client.chat.scheduleMessage({
2087
+ token,
2088
+ channel,
2089
+ thread_ts: threadTs || void 0,
2090
+ post_at: postAtUnix,
2091
+ text,
2092
+ unfurl_links: false,
2093
+ unfurl_media: false
2094
+ });
2095
+ const scheduledMessageId = result.scheduled_message_id;
2096
+ const adapter = this;
2097
+ return {
2098
+ scheduledMessageId,
2099
+ channelId: channel,
2100
+ postAt: options.postAt,
2101
+ raw: result,
2102
+ async cancel() {
2103
+ await adapter.client.chat.deleteScheduledMessage({
2104
+ token,
2105
+ channel,
2106
+ scheduled_message_id: scheduledMessageId
2107
+ });
2108
+ }
2109
+ };
2110
+ } catch (error) {
2111
+ this.handleSlackError(error);
2112
+ }
2113
+ }
1817
2114
  async openModal(triggerId, modal, contextId) {
1818
2115
  const metadata = encodeModalMetadata({
1819
2116
  contextId,
@@ -1958,6 +2255,31 @@ var SlackAdapter = class _SlackAdapter {
1958
2255
  raw: result2
1959
2256
  };
1960
2257
  }
2258
+ const tableResult = this.renderWithTableBlocks(message);
2259
+ if (tableResult) {
2260
+ this.logger.debug("Slack API: chat.update (table blocks)", {
2261
+ channel,
2262
+ messageId,
2263
+ blockCount: tableResult.blocks.length
2264
+ });
2265
+ const result2 = await this.client.chat.update(
2266
+ this.withToken({
2267
+ channel,
2268
+ ts: messageId,
2269
+ text: tableResult.text,
2270
+ blocks: tableResult.blocks
2271
+ })
2272
+ );
2273
+ this.logger.debug("Slack API: chat.update response", {
2274
+ messageId: result2.ts,
2275
+ ok: result2.ok
2276
+ });
2277
+ return {
2278
+ id: result2.ts,
2279
+ threadId,
2280
+ raw: result2
2281
+ };
2282
+ }
1961
2283
  const text = convertEmojiPlaceholders(
1962
2284
  this.formatConverter.renderPostable(message),
1963
2285
  "slack"
@@ -2106,9 +2428,9 @@ var SlackAdapter = class _SlackAdapter {
2106
2428
  */
2107
2429
  async stream(threadId, textStream, options) {
2108
2430
  if (!(options?.recipientUserId && options?.recipientTeamId)) {
2109
- throw new ChatError(
2110
- "Slack streaming requires recipientUserId and recipientTeamId in options",
2111
- "MISSING_STREAM_OPTIONS"
2431
+ throw new ValidationError(
2432
+ "slack",
2433
+ "Slack streaming requires recipientUserId and recipientTeamId in options"
2112
2434
  );
2113
2435
  }
2114
2436
  const { channel, threadTs } = this.decodeThreadId(threadId);
@@ -2751,9 +3073,9 @@ var SlackAdapter = class _SlackAdapter {
2751
3073
  } else {
2752
3074
  const message = options?.message;
2753
3075
  if (!message) {
2754
- throw new ChatError(
2755
- "Message required for replace action",
2756
- "INVALID_ARGS"
3076
+ throw new ValidationError(
3077
+ "slack",
3078
+ "Message required for replace action"
2757
3079
  );
2758
3080
  }
2759
3081
  const card = extractCard(message);
@@ -2764,13 +3086,22 @@ var SlackAdapter = class _SlackAdapter {
2764
3086
  blocks: cardToBlockKit(card)
2765
3087
  };
2766
3088
  } else {
2767
- payload = {
2768
- replace_original: true,
2769
- text: convertEmojiPlaceholders(
2770
- this.formatConverter.renderPostable(message),
2771
- "slack"
2772
- )
2773
- };
3089
+ const tableResult = this.renderWithTableBlocks(message);
3090
+ if (tableResult) {
3091
+ payload = {
3092
+ replace_original: true,
3093
+ text: tableResult.text,
3094
+ blocks: tableResult.blocks
3095
+ };
3096
+ } else {
3097
+ payload = {
3098
+ replace_original: true,
3099
+ text: convertEmojiPlaceholders(
3100
+ this.formatConverter.renderPostable(message),
3101
+ "slack"
3102
+ )
3103
+ };
3104
+ }
2774
3105
  }
2775
3106
  if (options?.threadTs) {
2776
3107
  payload.thread_ts = options.threadTs;
@@ -2792,9 +3123,9 @@ var SlackAdapter = class _SlackAdapter {
2792
3123
  status: response.status,
2793
3124
  body: errorText
2794
3125
  });
2795
- throw new ChatError(
2796
- `Failed to ${action} via response_url: ${errorText}`,
2797
- response.status.toString()
3126
+ throw new NetworkError(
3127
+ "slack",
3128
+ `Failed to ${action} via response_url: ${errorText}`
2798
3129
  );
2799
3130
  }
2800
3131
  const responseText = await response.text();