@timqi/pier 0.1.0 → 0.1.1

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 (158) hide show
  1. package/README.md +58 -125
  2. package/dist/agent/config.js +6 -15
  3. package/dist/agent/credentials.js +11 -23
  4. package/dist/agent/events.js +42 -64
  5. package/dist/agent/listing.js +39 -92
  6. package/dist/agent/pi.js +103 -252
  7. package/dist/boards/boards.js +19 -29
  8. package/dist/channels/attach.js +14 -42
  9. package/dist/channels/chains.js +33 -37
  10. package/dist/channels/chunk.js +8 -28
  11. package/dist/channels/commands.js +3 -14
  12. package/dist/channels/config.js +33 -52
  13. package/dist/channels/control.js +4 -13
  14. package/dist/channels/conversations.js +8 -25
  15. package/dist/channels/dedup.js +8 -17
  16. package/dist/channels/gatekeeper.js +13 -23
  17. package/dist/channels/lark-api.js +23 -63
  18. package/dist/channels/lark-outbound.js +12 -44
  19. package/dist/channels/lark-panel.js +7 -23
  20. package/dist/channels/lark-render.js +18 -62
  21. package/dist/channels/lark.js +52 -141
  22. package/dist/channels/lines.js +13 -15
  23. package/dist/channels/panel.js +16 -36
  24. package/dist/channels/receipts.js +29 -52
  25. package/dist/channels/routes.js +3 -9
  26. package/dist/channels/runtime.js +12 -23
  27. package/dist/channels/slack-api.js +34 -86
  28. package/dist/channels/slack-directory.js +7 -23
  29. package/dist/channels/slack-outbound.js +12 -56
  30. package/dist/channels/slack-panel.js +4 -13
  31. package/dist/channels/slack-render.js +23 -91
  32. package/dist/channels/slack-tool.js +48 -171
  33. package/dist/channels/slack.js +73 -239
  34. package/dist/channels/telegram-api.js +8 -20
  35. package/dist/channels/telegram-panel.js +5 -21
  36. package/dist/channels/telegram-render.js +13 -40
  37. package/dist/channels/telegram.js +54 -146
  38. package/dist/channels/types.js +5 -16
  39. package/dist/cli.js +17 -41
  40. package/dist/config-sync.js +87 -4
  41. package/dist/core/hub.js +7 -20
  42. package/dist/core/identity.js +20 -59
  43. package/dist/core/inbound-file.js +15 -49
  44. package/dist/core/inbox.js +12 -34
  45. package/dist/core/queue.js +3 -5
  46. package/dist/core/reply.js +41 -142
  47. package/dist/core/router.js +209 -264
  48. package/dist/core/types.js +4 -0
  49. package/dist/db.js +88 -272
  50. package/dist/drain.js +57 -50
  51. package/dist/extensions/index.js +3 -11
  52. package/dist/extensions/web/anthropic.js +3 -9
  53. package/dist/extensions/web/artifacts.js +2 -5
  54. package/dist/extensions/web/content.js +4 -12
  55. package/dist/extensions/web/http.js +2 -6
  56. package/dist/extensions/web/language.js +8 -18
  57. package/dist/extensions/web/openai.js +1 -1
  58. package/dist/extensions/web/provider.js +5 -18
  59. package/dist/extensions/web/tools.js +19 -63
  60. package/dist/lock.js +98 -0
  61. package/dist/log.js +9 -26
  62. package/dist/main.js +84 -183
  63. package/dist/paths.js +10 -26
  64. package/dist/secrets.js +18 -45
  65. package/dist/service.js +31 -73
  66. package/dist/settings.js +19 -63
  67. package/dist/tasks/agent.js +24 -45
  68. package/dist/tasks/callbacks.js +8 -16
  69. package/dist/tasks/command.js +2 -6
  70. package/dist/tasks/definitions.js +39 -62
  71. package/dist/tasks/execution.js +41 -39
  72. package/dist/tasks/groups.js +8 -11
  73. package/dist/tasks/messages.js +88 -155
  74. package/dist/tasks/outbox.js +33 -54
  75. package/dist/tasks/routes.js +4 -7
  76. package/dist/tasks/runs.js +4 -9
  77. package/dist/tasks/service.js +29 -42
  78. package/dist/tasks/store.js +36 -27
  79. package/dist/tasks/tool.js +59 -60
  80. package/dist/tools-task.js +20 -60
  81. package/dist/tools.js +98 -325
  82. package/dist/update.js +20 -43
  83. package/dist/web/auth.js +118 -179
  84. package/dist/web/config-sync.js +2 -2
  85. package/dist/web/config.js +3 -7
  86. package/dist/web/explorer.js +10 -21
  87. package/dist/web/fs.js +20 -42
  88. package/dist/web/instance.js +35 -82
  89. package/dist/web/providers.js +5 -11
  90. package/dist/web/public/assets/{activity-Bl3vZukb.js → activity-B89_hH7q.js} +1 -1
  91. package/dist/web/public/assets/activity-B89_hH7q.js.br +0 -0
  92. package/dist/web/public/assets/activity-B89_hH7q.js.gz +0 -0
  93. package/dist/web/public/assets/{boards-DYuf4Mlj.js → boards-BeKW0ZXK.js} +1 -1
  94. package/dist/web/public/assets/boards-BeKW0ZXK.js.br +0 -0
  95. package/dist/web/public/assets/boards-BeKW0ZXK.js.gz +0 -0
  96. package/dist/web/public/assets/explorer-DIuMlaV3.js +4 -0
  97. package/dist/web/public/assets/explorer-DIuMlaV3.js.br +0 -0
  98. package/dist/web/public/assets/explorer-DIuMlaV3.js.gz +0 -0
  99. package/dist/web/public/assets/index-DzXDXra_.js +85 -0
  100. package/dist/web/public/assets/index-DzXDXra_.js.br +0 -0
  101. package/dist/web/public/assets/index-DzXDXra_.js.gz +0 -0
  102. package/dist/web/public/assets/index-eqQLVS8Q.css +2 -0
  103. package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
  104. package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
  105. package/dist/web/public/assets/{runs-BLJu7EXN.js → runs-Cwy0mN8i.js} +1 -1
  106. package/dist/web/public/assets/runs-Cwy0mN8i.js.br +0 -0
  107. package/dist/web/public/assets/runs-Cwy0mN8i.js.gz +0 -0
  108. package/dist/web/public/assets/{settings-BrdVh-Zi.js → settings-DzZLmujq.js} +1 -1
  109. package/dist/web/public/assets/settings-DzZLmujq.js.br +0 -0
  110. package/dist/web/public/assets/settings-DzZLmujq.js.gz +0 -0
  111. package/dist/web/public/assets/{task-runs-CeQS1rxa.js → task-runs-BCakxFk8.js} +1 -1
  112. package/dist/web/public/assets/task-runs-BCakxFk8.js.br +0 -0
  113. package/dist/web/public/assets/task-runs-BCakxFk8.js.gz +0 -0
  114. package/dist/web/public/assets/tasks-BlzEbk11.js +4 -0
  115. package/dist/web/public/assets/tasks-BlzEbk11.js.br +0 -0
  116. package/dist/web/public/assets/tasks-BlzEbk11.js.gz +0 -0
  117. package/dist/web/public/index.html +30 -16
  118. package/dist/web/public/index.html.br +0 -0
  119. package/dist/web/public/index.html.gz +0 -0
  120. package/dist/web/public/sw.js +14 -2
  121. package/dist/web/public/sw.js.br +0 -0
  122. package/dist/web/public/sw.js.gz +0 -0
  123. package/dist/web/push.js +55 -77
  124. package/dist/web/route.js +3 -7
  125. package/dist/web/server.js +109 -190
  126. package/dist/web/session-state.js +13 -53
  127. package/dist/web/types.js +2 -4
  128. package/dist/web/webpush.js +10 -25
  129. package/docs/deploy.md +115 -330
  130. package/package.json +1 -1
  131. package/skills/pier-boards/SKILL.md +81 -160
  132. package/skills/pier-help/SKILL.md +23 -20
  133. package/skills/pier-slack/SKILL.md +2 -2
  134. package/skills/pier-tasks/SKILL.md +23 -15
  135. package/dist/config-sync-fetch.js +0 -84
  136. package/dist/limits.js +0 -14
  137. package/dist/web/public/assets/activity-Bl3vZukb.js.br +0 -0
  138. package/dist/web/public/assets/activity-Bl3vZukb.js.gz +0 -0
  139. package/dist/web/public/assets/boards-DYuf4Mlj.js.br +0 -0
  140. package/dist/web/public/assets/boards-DYuf4Mlj.js.gz +0 -0
  141. package/dist/web/public/assets/explorer-qJH_9nTE.js +0 -4
  142. package/dist/web/public/assets/explorer-qJH_9nTE.js.br +0 -0
  143. package/dist/web/public/assets/explorer-qJH_9nTE.js.gz +0 -0
  144. package/dist/web/public/assets/index-Dqdb-Eqt.js +0 -85
  145. package/dist/web/public/assets/index-Dqdb-Eqt.js.br +0 -0
  146. package/dist/web/public/assets/index-Dqdb-Eqt.js.gz +0 -0
  147. package/dist/web/public/assets/index-DzmMzvi_.css +0 -2
  148. package/dist/web/public/assets/index-DzmMzvi_.css.br +0 -0
  149. package/dist/web/public/assets/index-DzmMzvi_.css.gz +0 -0
  150. package/dist/web/public/assets/runs-BLJu7EXN.js.br +0 -0
  151. package/dist/web/public/assets/runs-BLJu7EXN.js.gz +0 -0
  152. package/dist/web/public/assets/settings-BrdVh-Zi.js.br +0 -0
  153. package/dist/web/public/assets/settings-BrdVh-Zi.js.gz +0 -0
  154. package/dist/web/public/assets/task-runs-CeQS1rxa.js.br +0 -0
  155. package/dist/web/public/assets/task-runs-CeQS1rxa.js.gz +0 -0
  156. package/dist/web/public/assets/tasks-bcb3fYdK.js +0 -4
  157. package/dist/web/public/assets/tasks-bcb3fYdK.js.br +0 -0
  158. package/dist/web/public/assets/tasks-bcb3fYdK.js.gz +0 -0
@@ -1,47 +1,26 @@
1
- // How a turn becomes messages in a Slack thread.
2
- //
3
- // Split out of the adapter because it is a separate decision from routing
4
- // inbound traffic: which renderer to use, how to chunk against that renderer's
5
- // limit, and what an empty turn still has to say. The adapter keeps the 👀
6
- // receipts, because those are about the turn ending, not about what was said.
1
+ // How a turn becomes messages in a Slack thread: which renderer, how to chunk
2
+ // against its limit, and what an empty turn still has to say.
7
3
  import { formatTurnMeta, isSilentReply, originLabel, quietLabel } from "../core/reply.js";
8
4
  import { sendAttachments, splitAttachments } from "./attach.js";
9
5
  import { isBlockRejection } from "./slack-api.js";
10
6
  import { actions, chunk, context, escapeMrkdwn, markdown, MARKDOWN_MAX, MRKDWN_MAX, sections, toMrkdwn, } from "./slack-render.js";
11
- /**
12
- * The web shows a turn's cost on hover. Slack has a `context` block — genuinely
13
- * small, muted text — so unlike Telegram the footer needs no italic hack to
14
- * read as a footnote.
15
- */
16
7
  const footerText = (meta) => escapeMrkdwn(formatTurnMeta(meta));
17
8
  export class SlackOutbound {
18
9
  api;
19
10
  log;
20
- /**
21
- * Latched off for the process on the first refusal, so the failed round trip
22
- * is paid once rather than once per message.
23
- */
11
+ /** Latched off on the first refusal, so the failed round trip is paid once. */
24
12
  markdownBlocks = true;
25
13
  constructor(api, log) {
26
14
  this.api = api;
27
15
  this.log = log;
28
16
  }
29
- /**
30
- * Post one turn, empty text included: the turn settled with nothing to say,
31
- * and that is still something to show.
32
- */
33
17
  async reply(channel, threadTs, reply) {
34
- // A file the agent linked lives on Pier's machine, so the link is dead in
35
- // Slack: the bytes are uploaded instead and the label stays in the text.
18
+ // A local file link is dead in Slack: the bytes are uploaded instead.
36
19
  const { text: spoken, paths } = splitAttachments(reply.text);
37
20
  const text = spoken.trim();
38
21
  const footer = reply.meta ? footerText(reply.meta) : "";
39
22
  const row = actions(reply.suggestions);
40
- // A turn that produced no text still posts its footer, and says which kind
41
- // of nothing it was. Silence must be *observable*: total silence is
42
- // indistinguishable from a crash, a dropped connection or a bug, and the
43
- // person waiting has no way to tell. A muted one-liner is the cheapest
44
- // honest answer.
23
+ // An empty turn still posts its footer and says which kind of nothing (§5).
45
24
  const quiet = isSilentReply(reply)
46
25
  ? `_${quietLabel(reply.silence && escapeMrkdwn(reply.silence))}_`
47
26
  : "";
@@ -50,33 +29,20 @@ export class SlackOutbound {
50
29
  const parts = text ? chunk(text, this.budget()) : [""];
51
30
  for (const [i, part] of parts.entries()) {
52
31
  const last = i === parts.length - 1;
53
- // The footer and the buttons ride the last chunk only. The quiet marker
54
- // shares the footer's block, so an empty turn is one muted line rather
55
- // than two.
32
+ // The quiet marker shares the footer's block: one muted line, not two.
56
33
  const note = last ? [quiet, footer].filter(Boolean).join(" · ") : "";
57
34
  await this.post(channel, threadTs, part, [
58
35
  ...(note ? [context(note)] : []),
59
36
  ...(last && row ? [row] : []),
60
37
  ]);
61
38
  }
62
- // Attachments follow the words, so the message introducing them is above
63
- // them; anything that could not be sent says so in the thread.
64
39
  const lost = await sendAttachments(paths, (file) => this.api.uploadFile(channel, threadTs, file), this.log);
65
- // Unescaped, like every other body: post() escapes on the path that needs it.
66
40
  if (lost)
67
41
  await this.post(channel, threadTs, lost, []);
68
42
  }
69
- /**
70
- * A system note: quoted, labelled with where it came from, and deliberately
71
- * plain — no buttons and no turn footer, because the turn this input
72
- * triggers has not ended yet.
73
- *
74
- * Answers with the `ts` of the last message it posted — where the caller
75
- * puts the 👀 for that turn, at the foot of the thread the reply will land
76
- * in. Undefined when there was nothing to post.
77
- */
43
+ /** No footer: the turn this input triggers has not ended. Answers with the
44
+ * `ts` of the last message posted, where the caller puts the 👀. */
78
45
  async note(channel, threadTs, note) {
79
- // Markdown's own blockquote, so the note reads as quoted on either path.
80
46
  const body = note.text.split("\n").map((line) => `> ${line}`).join("\n");
81
47
  let ts;
82
48
  for (const part of chunk(`_${originLabel(note.origin)}_\n${body}`, this.budget())) {
@@ -84,22 +50,13 @@ export class SlackOutbound {
84
50
  }
85
51
  return ts;
86
52
  }
87
- /** Which budget `chunk()` should respect, given the path we are on. */
88
53
  budget() {
89
54
  return this.markdownBlocks ? MARKDOWN_MAX : MRKDWN_MAX;
90
55
  }
91
- /**
92
- * Post one message's body, preferring Slack's own markdown renderer.
93
- *
94
- * The `markdown` block takes the agent's markdown unmodified — tables,
95
- * headers and nested lists all survive, none of which the mrkdwn subset can
96
- * express — and the client never folds it behind "Show more". It is recent
97
- * enough to be refused by an older workspace, so a rejection degrades to the
98
- * translated mrkdwn path instead of losing the turn.
99
- */
56
+ /** The `markdown` block is recent enough to be refused by an older
57
+ * workspace; a rejection degrades to the translated mrkdwn path. */
100
58
  async post(channel, threadTs, body, trailing) {
101
- // `text` is the notification and accessibility fallback, never shown
102
- // beside the blocks.
59
+ // `text` is the notification fallback, never shown beside the blocks.
103
60
  const notice = body || trailing.length ? body || "…" : "";
104
61
  if (this.markdownBlocks) {
105
62
  const blocks = [...(body ? [markdown(body)] : []), ...trailing];
@@ -116,8 +73,7 @@ export class SlackOutbound {
116
73
  this.log(`markdown block refused, falling back to mrkdwn: ${String(err)}`);
117
74
  }
118
75
  }
119
- // Legacy path: translate to mrkdwn and split into section blocks. The body
120
- // was chunked against the larger budget, so it may need splitting again.
76
+ // The body was chunked against the larger budget, so it may split again.
121
77
  let ts;
122
78
  for (const part of body ? chunk(toMrkdwn(body), MRKDWN_MAX) : [""]) {
123
79
  const blocks = [...sections(part), ...trailing];
@@ -1,12 +1,8 @@
1
- // Slack's half of the settings panel: mrkdwn markup, Block Kit, and a modal
2
- // for the one typed answer. The panel itself lives in `panel.ts`.
3
- //
4
- // The modal is what makes this half smaller than Telegram's: `private_metadata`
5
- // carries the conversation with the dialog, so a submitted path needs no
6
- // adapter-side state to be understood, and survives a reload.
1
+ // Slack's half of the settings panel (panel.ts has the rest). The modal's
2
+ // `private_metadata` carries the conversation, so a submission needs no
3
+ // adapter-side state and survives a reload.
7
4
  import { ChatPanel, CWD_PLACEHOLDER, CWD_TAIL, PANEL_PREFIX, } from "./panel.js";
8
5
  import { context, escapeMrkdwn as esc, section } from "./slack-render.js";
9
- /** The modal's ids. `private_metadata` carries which conversation it is for. */
10
6
  const CWD_VIEW = "cfg_cwd";
11
7
  const CWD_BLOCK = "cwd_block";
12
8
  const CWD_INPUT = "cwd_input";
@@ -34,13 +30,11 @@ export class SlackPanel extends ChatPanel {
34
30
  blocks(view, note) {
35
31
  return [
36
32
  ...view.groups.map((g) => section([`*${g.title}*${g.suffix ?? ""}`, ...g.lines].join("\n"))),
37
- // Slack fits a page of choices on one row; Telegram would not.
38
33
  ...(view.picks?.length ? [row(view.picks)] : []),
39
34
  ...view.rows.filter((r) => r.length).map(row),
40
35
  ...(note ? [context(esc(note))] : []),
41
36
  ];
42
37
  }
43
- /** Open a fresh panel, replacing whichever one this conversation had. */
44
38
  async open(key, channel, threadTs) {
45
39
  const sent = await this.deps.api.postMessage({
46
40
  channel,
@@ -63,10 +57,7 @@ export class SlackPanel extends ChatPanel {
63
57
  .catch((err) => this.deps.log(`panel close failed: ${String(err)}`));
64
58
  }
65
59
  // --- actions ---------------------------------------------------------------
66
- /**
67
- * Handle a `cfg:` click. Returns false when the action is not ours, so the
68
- * caller can treat it as one of the agent's next-step labels instead.
69
- */
60
+ /** Returns false when the action is not ours. */
70
61
  async onAction(interaction, key, actionId) {
71
62
  return this.dispatch(key, actionId, interaction, async () => {
72
63
  const channel = interaction.channel?.id;
@@ -1,78 +1,44 @@
1
1
  // How a reply looks on Slack: mrkdwn text, and buttons as a Block Kit
2
- // `actions` row.
3
- //
4
- // mrkdwn is not markdown. Bold is `*one*` star, italic is `_underscore_`,
5
- // strikethrough is `~one~` tilde, and a link is `<url|label>` — so the agent's
6
- // markdown has to be translated, not passed through. Only `&`, `<` and `>` are
7
- // escaped; unlike Telegram's HTML parser Slack degrades unknown syntax to
8
- // literal text instead of rejecting the message, so the risk here is an ugly
9
- // reply rather than a lost one.
2
+ // `actions` row. mrkdwn is not markdown (`*bold*`, `_italic_`, `~strike~`,
3
+ // `<url|label>`), so the agent's markdown is translated; Slack degrades unknown
4
+ // syntax to literal text rather than rejecting the message.
10
5
  import { balanceFences, chunkText } from "./chunk.js";
11
- /**
12
- * A `markdown` block's budget: Slack caps them at 12,000 cumulative chars per
13
- * message, and one message carries one. This is the normal path.
14
- */
6
+ /** Slack caps `markdown` blocks at 12,000 cumulative chars per message. */
15
7
  export const MARKDOWN_MAX = 11_000;
16
- /**
17
- * The legacy fallback's budget: a `section` block's text caps at 3000, and the
18
- * mrkdwn translation adds a little markup.
19
- */
8
+ /** A `section` block's text caps at 3000, and the mrkdwn translation adds markup. */
20
9
  export const MRKDWN_MAX = 2800;
21
10
  // Slack truncates a button label past this, mid-word.
22
11
  const BUTTON_MAX = 75;
23
- /** Shared: the adapter and the panel escape plain text with this too. */
24
12
  export const escapeMrkdwn = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
25
- /**
26
- * Inline emphasis, applied after escaping so our own markup stays ours.
27
- *
28
- * Bold is marked with a private-use sentinel rather than written as `*` right
29
- * away: mrkdwn spells bold with the single star that markdown uses for italic,
30
- * so emitting it early would let the italic pass eat it again.
31
- */
13
+ /** mrkdwn spells bold with the single star markdown uses for italic, so bold
14
+ * is marked with a sentinel until the italic pass has run. */
32
15
  const BOLD = "\uE002";
33
16
  function inline(text) {
34
17
  return text
35
- // Links first: their label may itself carry emphasis. Slack inverts the
36
- // order of markdown's pair, and `>` inside is already escaped.
18
+ // Links first: their label may itself carry emphasis.
37
19
  .replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g, (_m, label, url) => `<${url}|${label}>`)
38
20
  // Bold before italic: `**x**` must not be seen as two `*x*` runs.
39
21
  .replace(/\*\*([^\n*]+)\*\*/g, `${BOLD}$1${BOLD}`)
40
22
  .replace(/~~([^\n~]+)~~/g, "~$1~")
41
23
  .replace(/(^|[\s(])[*_]([^\n*_]+)[*_](?=[\s).,!?:;]|$)/g, "$1_$2_")
42
- // Headings have no size in Slack either; bold is the closest honest render.
43
24
  .replace(/^#{1,6}[ \t]+(.+)$/gm, `${BOLD}$1${BOLD}`)
44
- // Slack renders neither `-` nor `*` as a list marker, so bullets are drawn.
25
+ // Slack renders neither `-` nor `*` as a list marker.
45
26
  .replace(/^[ \t]*[-*+][ \t]+/gm, "\u2022 ")
46
27
  .replaceAll(BOLD, "*");
47
28
  }
48
- /**
49
- * Render one assistant turn as mrkdwn. Code spans and fences are extracted
50
- * before escaping so emphasis inside them stays literal.
51
- */
29
+ /** Code is stashed before escaping so emphasis inside it stays literal. */
52
30
  export function toMrkdwn(markdown) {
53
31
  const stash = [];
54
- // Private-use sentinels: markdown cannot contain them, so a stashed block
55
- // cannot be re-matched by the escaping and emphasis passes that follow.
56
32
  const keep = (text) => `\uE000${stash.push(text) - 1}\uE001`;
57
- // Slack code fences carry no language, so the hint is dropped rather than
58
- // shown as the first line of the block.
33
+ // Slack fences carry no language; the hint would show as the first line.
59
34
  let out = markdown.replace(/```[\w.+-]*\n?([\s\S]*?)```/g, (_m, code) => keep("```\n" + escapeMrkdwn(code.replace(/\n+$/, "")) + "\n```"));
60
35
  out = out.replace(/`([^`\n]+)`/g, (_m, code) => keep(`\`${escapeMrkdwn(code)}\``));
61
36
  out = inline(escapeMrkdwn(out));
62
37
  return out.replace(/\uE000(\d+)\uE001/g, (_m, i) => stash[Number(i)] ?? "");
63
38
  }
64
- /**
65
- * Split rendered mrkdwn into sendable chunks at the last blank line or newline
66
- * that fits, then re-balance code fences across the cut (see chunk.ts for why
67
- * an unbalanced fence is a mangled reply here and not on Telegram).
68
- */
69
39
  export const chunk = (text, max) => balanceFences(chunkText(text, max));
70
- /**
71
- * The body of a turn, as Slack's own markdown renderer sees it. Preferred over
72
- * `section` for everything the agent wrote: it takes the markdown unmodified
73
- * (so tables and headers survive) and the client never folds it behind
74
- * "Show more".
75
- */
40
+ /** Slack's own renderer: tables and headers survive, and the client never
41
+ * folds it behind "Show more". */
76
42
  export const markdown = (text) => ({ type: "markdown", text });
77
43
  export const section = (text) => ({
78
44
  type: "section",
@@ -80,14 +46,9 @@ export const section = (text) => ({
80
46
  });
81
47
  // Slack caps a message at 50 blocks; the footer and the button row need two.
82
48
  const MAX_BLOCKS = 45;
83
- // A section block's hard limit. Nothing should reach it — chunk() caps a whole
84
- // message below this — but the overflow merge below could in principle.
49
+ // A section block's hard limit; only the overflow merge in sections() can reach it.
85
50
  const SECTION_MAX = 2900;
86
- /**
87
- * Split rendered mrkdwn into paragraphs without ever cutting a fenced code
88
- * block — a fence split across two blocks would leave both unbalanced, the
89
- * same hazard `chunk()` handles for messages.
90
- */
51
+ /** Never cuts a fenced block: a fence split across two blocks leaves both unbalanced. */
91
52
  function paragraphs(text) {
92
53
  const out = [];
93
54
  let buf = [];
@@ -99,7 +60,6 @@ function paragraphs(text) {
99
60
  };
100
61
  for (const line of text.split("\n")) {
101
62
  const isFence = line.trimStart().startsWith("```");
102
- // A blank line only ends a paragraph outside a fence; inside one it is code.
103
63
  if (!fenced && !isFence && !line.trim()) {
104
64
  flush();
105
65
  continue;
@@ -107,7 +67,6 @@ function paragraphs(text) {
107
67
  buf.push(line);
108
68
  if (isFence) {
109
69
  fenced = !fenced;
110
- // A closed fence stands alone, so it can never be merged apart.
111
70
  if (!fenced)
112
71
  flush();
113
72
  }
@@ -115,52 +74,31 @@ function paragraphs(text) {
115
74
  flush();
116
75
  return out;
117
76
  }
118
- /**
119
- * The body of one message as one `section` block per paragraph — the fallback
120
- * for a workspace whose Slack refuses the `markdown` block.
121
- *
122
- * A whole turn in a single section block gets collapsed behind "Show more",
123
- * hiding most of the answer; several blocks render unfolded. Paragraphs are
124
- * deliberately *not* packed together to fill a size budget — a paragraph is
125
- * already the natural short unit, and merging a few of them back into one tall
126
- * block is exactly what brings the collapse back.
127
- */
77
+ /** The fallback for a workspace that refuses the `markdown` block. One section
78
+ * per paragraph, never packed: a tall single block is collapsed behind "Show
79
+ * more", several short ones render unfolded. */
128
80
  export function sections(text) {
129
81
  const paras = paragraphs(text);
130
82
  if (!paras.length)
131
83
  return [];
132
- // Past the block cap the tail is folded into the last block rather than
133
- // dropped: a truncated reply is worse than a tall one, and silently losing
134
- // the end of an answer is worst of all.
84
+ // The tail is folded into the last block, not dropped.
135
85
  const kept = paras.slice(0, MAX_BLOCKS - 1);
136
86
  const tail = paras.slice(MAX_BLOCKS - 1);
137
87
  if (tail.length)
138
88
  kept.push(tail.join("\n\n").slice(0, SECTION_MAX));
139
89
  return kept.map(section);
140
90
  }
141
- /**
142
- * Slack's small muted text. Telegram has none, which is why `formatTurnMeta`
143
- * lands there as an italic footnote; here the footer gets the block the
144
- * platform actually has for it.
145
- */
91
+ /** Slack's small muted text, for the footer. */
146
92
  export const context = (text) => ({
147
93
  type: "context",
148
94
  elements: [{ type: "mrkdwn", text }],
149
95
  });
150
96
  // --- next-step buttons -------------------------------------------------------
151
- /**
152
- * `action_id` carries an index, not the label. Slack would allow 2000 chars of
153
- * `value`, but the index is what makes a button survive a `runtime.reload()`:
154
- * the label is read back off the message Slack echoes with the click, so no
155
- * adapter-instance memory is involved.
156
- */
97
+ /** `action_id` carries an index: the label is read back off the message Slack
98
+ * echoes with the click, so a button survives a reload. */
157
99
  export const OFFER_PREFIX = "sg:";
158
100
  const truncate = (label) => label.length > BUTTON_MAX ? `${label.slice(0, BUTTON_MAX - 1)}\u2026` : label;
159
- /**
160
- * One actions row. Slack wraps buttons on its own and gives each its natural
161
- * width, so unlike Telegram there is no row packing to budget — a long label
162
- * beside a short one costs nothing.
163
- */
101
+ /** Slack wraps buttons on its own, so there is no row packing to budget. */
164
102
  export function actions(labels) {
165
103
  if (!labels.length)
166
104
  return undefined;
@@ -171,12 +109,6 @@ export function actions(labels) {
171
109
  }));
172
110
  return { type: "actions", elements };
173
111
  }
174
- /**
175
- * The label a next-step `action_id` stands for, read off the clicked message's
176
- * own blocks — which Slack echoes back in the interaction payload. A button
177
- * therefore keeps working across a restart or a config reload, where an
178
- * in-memory offer list would not.
179
- */
180
112
  export function offeredLabel(blocks, actionId) {
181
113
  if (!actionId.startsWith(OFFER_PREFIX))
182
114
  return undefined;