@s0nderlabs/anima-plugin-telegram 0.19.18 → 0.19.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@s0nderlabs/anima-plugin-telegram",
3
- "version": "0.19.18",
3
+ "version": "0.19.19",
4
4
  "type": "module",
5
5
  "description": "Telegram gateway plugin for anima — long-poll bot, debounced dispatch, reactions, allowlist",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@
28
28
  "test": "bun test"
29
29
  },
30
30
  "dependencies": {
31
- "@s0nderlabs/anima-core": "0.19.18",
31
+ "@s0nderlabs/anima-core": "0.19.19",
32
32
  "grammy": "^1.42.0",
33
33
  "zod": "^3.23.8"
34
34
  }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { formatApprovalResolution } from './listener'
3
+
4
+ /**
5
+ * Regression test for v0.19.19: every choice maps to a human-readable
6
+ * suffix so the post-click modal edit shows the operator what they tapped.
7
+ * v0.19.18 left modals visible after click because the listener only
8
+ * answered the popup but never edited the message body. v0.19.19 edits the
9
+ * text to append this suffix and removes the inline keyboard.
10
+ */
11
+ describe('formatApprovalResolution', () => {
12
+ it('labels each choice distinctly with the clicker user id', () => {
13
+ expect(formatApprovalResolution('once', 42)).toBe('✅ Allowed once (by 42)')
14
+ expect(formatApprovalResolution('session', 42)).toBe('✅ Allowed for session (by 42)')
15
+ expect(formatApprovalResolution('always', 42)).toBe('✅ Always allowed (by 42)')
16
+ expect(formatApprovalResolution('deny', 42)).toBe('❌ Denied (by 42)')
17
+ })
18
+
19
+ it('uses ✅ for permitting choices and ❌ only for deny', () => {
20
+ for (const choice of ['once', 'session', 'always'] as const) {
21
+ expect(formatApprovalResolution(choice, 1)).toMatch(/^✅/)
22
+ }
23
+ expect(formatApprovalResolution('deny', 1)).toMatch(/^❌/)
24
+ })
25
+ })
package/src/index.ts CHANGED
@@ -27,7 +27,12 @@ export type {
27
27
  TelegramToolEvent,
28
28
  } from './types'
29
29
  export { ProgressTracker, PROGRESS_EDIT_INTERVAL } from './progress'
30
- export { TelegramListener, TELEGRAM_ALLOWED_UPDATES, capForTelegram } from './listener'
30
+ export {
31
+ TelegramListener,
32
+ TELEGRAM_ALLOWED_UPDATES,
33
+ capForTelegram,
34
+ formatApprovalResolution,
35
+ } from './listener'
31
36
  export { buildSessionKey, sanitizeAgentName } from './session-key'
32
37
  export { formatTelegramChannel, formatInboundPreview } from './format'
33
38
  export { RateLimiter } from './limits'
package/src/listener.ts CHANGED
@@ -38,6 +38,23 @@ import { startTypingLoop } from './typing'
38
38
  const RETRY_INTERVAL_MS = 30_000
39
39
  const MAX_LOCK_RETRY_ATTEMPTS = 12
40
40
 
41
+ /**
42
+ * Map an `ApprovalChoice` to the human-readable resolution label appended
43
+ * to the approval message after a click. Mirrors the keyboard labels so
44
+ * operators see the same wording in the resolved message that they tapped.
45
+ */
46
+ export function formatApprovalResolution(choice: ApprovalChoice, byUserId: number): string {
47
+ const label =
48
+ choice === 'once'
49
+ ? '✅ Allowed once'
50
+ : choice === 'session'
51
+ ? '✅ Allowed for session'
52
+ : choice === 'always'
53
+ ? '✅ Always allowed'
54
+ : '❌ Denied'
55
+ return `${label} (by ${byUserId})`
56
+ }
57
+
41
58
  /**
42
59
  * Update kinds we ask Telegram to deliver via long-poll.
43
60
  *
@@ -136,6 +153,24 @@ export class TelegramListener {
136
153
  } catch {
137
154
  /* ignore */
138
155
  }
156
+ // Resolve the modal visually: append the choice + drop the inline
157
+ // keyboard. Without this, every clicked approval message stays on
158
+ // screen with all four buttons, leaving operators unsure whether
159
+ // their tap registered. Best-effort — if the edit fails (rate
160
+ // limit, message age, deleted), the underlying approval is still
161
+ // resolved at the runtime level, so we swallow the error.
162
+ const originalText =
163
+ typeof q.message?.text === 'string' && q.message.text.length > 0 ? q.message.text : null
164
+ const suffix = formatApprovalResolution(parsed.choice, q.from.id)
165
+ try {
166
+ if (originalText) {
167
+ await ctx.editMessageText(`${originalText}\n\n${suffix}`, { reply_markup: undefined })
168
+ } else {
169
+ await ctx.editMessageReplyMarkup({ reply_markup: undefined })
170
+ }
171
+ } catch {
172
+ /* ignore — modal was clicked, runtime already resolved; keyboard cleanup is cosmetic */
173
+ }
139
174
  }
140
175
 
141
176
  async start(): Promise<void> {