switchroom 0.16.23 → 0.16.24

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 (127) hide show
  1. package/dist/agent-scheduler/index.js +80 -80
  2. package/dist/auth-broker/index.js +80 -80
  3. package/dist/cli/autoaccept-poll.js +8 -8
  4. package/dist/cli/drive-write-pretool.mjs +10 -10
  5. package/dist/cli/notion-write-pretool.mjs +82 -82
  6. package/dist/cli/self-improve-apply-guard-pretool.mjs +6 -0
  7. package/dist/cli/skill-validate-pretool.mjs +2936 -119
  8. package/dist/cli/switchroom.js +671 -430
  9. package/dist/host-control/main.js +156 -156
  10. package/dist/vault/approvals/kernel-server.js +82 -82
  11. package/dist/vault/broker/server.js +83 -83
  12. package/package.json +5 -5
  13. package/telegram-plugin/answer-stream.ts +13 -43
  14. package/telegram-plugin/auth-snapshot-format.ts +27 -30
  15. package/telegram-plugin/auto-fallback-fleet.ts +6 -11
  16. package/telegram-plugin/card-format.ts +28 -25
  17. package/telegram-plugin/credits-watch.ts +5 -10
  18. package/telegram-plugin/dist/bridge/bridge.js +112 -112
  19. package/telegram-plugin/dist/gateway/gateway.js +1855 -2077
  20. package/telegram-plugin/dist/server.js +160 -160
  21. package/telegram-plugin/draft-stream.ts +4 -4
  22. package/telegram-plugin/format.ts +101 -662
  23. package/telegram-plugin/gateway/approval-callback.ts +2 -3
  24. package/telegram-plugin/gateway/approval-card.test.ts +17 -4
  25. package/telegram-plugin/gateway/approval-card.ts +16 -6
  26. package/telegram-plugin/gateway/approvals-commands.ts +18 -24
  27. package/telegram-plugin/gateway/auth-command.ts +74 -74
  28. package/telegram-plugin/gateway/auth-line.ts +5 -15
  29. package/telegram-plugin/gateway/boot-card.ts +20 -22
  30. package/telegram-plugin/gateway/boot-version.ts +3 -2
  31. package/telegram-plugin/gateway/config-approval-handler.test.ts +35 -33
  32. package/telegram-plugin/gateway/config-approval-handler.ts +24 -24
  33. package/telegram-plugin/gateway/config-snapshot.ts +9 -9
  34. package/telegram-plugin/gateway/diff-preview-card.test.ts +8 -8
  35. package/telegram-plugin/gateway/diff-preview-card.ts +2 -5
  36. package/telegram-plugin/gateway/disconnect-flush.ts +0 -4
  37. package/telegram-plugin/gateway/effort-command.ts +17 -17
  38. package/telegram-plugin/gateway/folder-picker-handler.test.ts +8 -2
  39. package/telegram-plugin/gateway/folder-picker-handler.ts +3 -4
  40. package/telegram-plugin/gateway/gateway.ts +751 -624
  41. package/telegram-plugin/gateway/inject-handler.test.ts +15 -13
  42. package/telegram-plugin/gateway/inject-handler.ts +5 -5
  43. package/telegram-plugin/gateway/ipc-protocol.ts +33 -1
  44. package/telegram-plugin/gateway/ipc-server.ts +28 -0
  45. package/telegram-plugin/gateway/linear-activity.ts +16 -14
  46. package/telegram-plugin/gateway/linear-setup.ts +1 -1
  47. package/telegram-plugin/gateway/model-command.ts +25 -25
  48. package/telegram-plugin/gateway/oversize-card-body.ts +6 -7
  49. package/telegram-plugin/gateway/skill-proposal-card.ts +167 -0
  50. package/telegram-plugin/inline-keyboard-callbacks.ts +19 -13
  51. package/telegram-plugin/issues-card.ts +6 -7
  52. package/telegram-plugin/model-unavailable.ts +8 -12
  53. package/telegram-plugin/operator-events-history.ts +1 -1
  54. package/telegram-plugin/operator-events.ts +24 -28
  55. package/telegram-plugin/package.json +1 -1
  56. package/telegram-plugin/pending-work-progress.ts +36 -36
  57. package/telegram-plugin/permission-title.ts +36 -20
  58. package/telegram-plugin/pty-partial-handler.ts +5 -13
  59. package/telegram-plugin/quota-check.ts +5 -5
  60. package/telegram-plugin/quota-watch.ts +13 -18
  61. package/telegram-plugin/recent-outbound-dedup.ts +5 -5
  62. package/telegram-plugin/registry/turns-schema.ts +43 -3
  63. package/telegram-plugin/retry-api-call.ts +15 -7
  64. package/telegram-plugin/rich-send.ts +57 -0
  65. package/telegram-plugin/secret-detect/vault-error.test.ts +6 -6
  66. package/telegram-plugin/secret-detect/vault-error.ts +29 -22
  67. package/telegram-plugin/shared/bot-runtime.ts +23 -6
  68. package/telegram-plugin/silence-poke.ts +26 -69
  69. package/telegram-plugin/slot-banner-driver.ts +9 -6
  70. package/telegram-plugin/slot-banner.ts +5 -8
  71. package/telegram-plugin/steering.ts +0 -4
  72. package/telegram-plugin/stream-controller.ts +59 -62
  73. package/telegram-plugin/stream-reply-handler.ts +40 -97
  74. package/telegram-plugin/subagent-watcher.ts +2 -2
  75. package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +5 -2
  76. package/telegram-plugin/tests/answer-stream.test.ts +54 -63
  77. package/telegram-plugin/tests/auth-command-format2.test.ts +4 -4
  78. package/telegram-plugin/tests/auth-command-vernacular.test.ts +3 -2
  79. package/telegram-plugin/tests/auth-snapshot-format.test.ts +19 -18
  80. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +15 -13
  81. package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +27 -12
  82. package/telegram-plugin/tests/boot-card-render.test.ts +59 -48
  83. package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
  84. package/telegram-plugin/tests/bot-api.harness.ts +23 -1
  85. package/telegram-plugin/tests/bot-runtime.test.ts +23 -18
  86. package/telegram-plugin/tests/card-format.test.ts +6 -4
  87. package/telegram-plugin/tests/config-snapshot.test.ts +1 -1
  88. package/telegram-plugin/tests/credits-watch.test.ts +5 -5
  89. package/telegram-plugin/tests/fake-bot-api.ts +58 -4
  90. package/telegram-plugin/tests/finalize-callback.test.ts +11 -9
  91. package/telegram-plugin/tests/foreground-nesting.test.ts +1 -1
  92. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +3 -13
  93. package/telegram-plugin/tests/issues-card.test.ts +15 -12
  94. package/telegram-plugin/tests/linear-agent-activity.test.ts +8 -5
  95. package/telegram-plugin/tests/model-command.test.ts +2 -2
  96. package/telegram-plugin/tests/model-unavailable.test.ts +13 -13
  97. package/telegram-plugin/tests/multi-turn-continuity.test.ts +6 -10
  98. package/telegram-plugin/tests/operator-events.test.ts +7 -9
  99. package/telegram-plugin/tests/pending-work-progress.test.ts +20 -21
  100. package/telegram-plugin/tests/permission-title.test.ts +45 -41
  101. package/telegram-plugin/tests/pty-partial-handler.test.ts +8 -8
  102. package/telegram-plugin/tests/quota-check.test.ts +3 -3
  103. package/telegram-plugin/tests/quota-watch.test.ts +8 -4
  104. package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +4 -3
  105. package/telegram-plugin/tests/silence-poke.test.ts +75 -112
  106. package/telegram-plugin/tests/single-mode-stream-reply.test.ts +137 -0
  107. package/telegram-plugin/tests/skill-proposal-card.test.ts +103 -0
  108. package/telegram-plugin/tests/slot-banner-driver.e2e.test.ts +36 -24
  109. package/telegram-plugin/tests/slot-banner.test.ts +9 -6
  110. package/telegram-plugin/tests/status-accent.test.ts +29 -32
  111. package/telegram-plugin/tests/{stream-controller-html-fallback.test.ts → stream-controller-parse-fallback.test.ts} +40 -42
  112. package/telegram-plugin/tests/stream-controller.test.ts +63 -52
  113. package/telegram-plugin/tests/stream-reply-error-paths.test.ts +43 -38
  114. package/telegram-plugin/tests/stream-reply-handler.test.ts +122 -249
  115. package/telegram-plugin/tests/streaming-e2e.test.ts +35 -30
  116. package/telegram-plugin/tests/streaming-orchestration.test.ts +29 -28
  117. package/telegram-plugin/tests/telegram-format.test.ts +120 -1083
  118. package/telegram-plugin/tests/tool-activity-summary.test.ts +144 -145
  119. package/telegram-plugin/tests/welcome-text.test.ts +72 -65
  120. package/telegram-plugin/tests/worker-activity-feed.test.ts +119 -137
  121. package/telegram-plugin/text-voice-scrub.ts +8 -11
  122. package/telegram-plugin/tool-activity-summary.ts +29 -29
  123. package/telegram-plugin/welcome-text.ts +82 -83
  124. package/telegram-plugin/worker-activity-feed.ts +2 -3
  125. package/telegram-plugin/html-sanitize.ts +0 -244
  126. package/telegram-plugin/tests/html-sanitize.test.ts +0 -146
  127. package/telegram-plugin/tests/parse-mode-rotation.test.ts +0 -162
@@ -72,10 +72,9 @@ function setup(opts: { progressCardActive?: boolean } = {}): Fixture {
72
72
  ? { activeDraftStreams: map, suppressPtyPreview: suppress }
73
73
  : { activeDraftStreams: map }
74
74
 
75
- const pty = createPtyPartialHandler(ptyState, {
76
- bot,
77
- renderText: (t) => `<i>${t}</i>`, // PTY preview: italic
78
- })
75
+ // #2669: PTY previews are raw terminal output sent LITERALLY (plain
76
+ // sendMessage). The handler no longer takes a renderText.
77
+ const pty = createPtyPartialHandler(ptyState, { bot })
79
78
 
80
79
  const callStreamReply = (args: {
81
80
  chat_id: string
@@ -85,13 +84,13 @@ function setup(opts: { progressCardActive?: boolean } = {}): Fixture {
85
84
  }) =>
86
85
  handleStreamReply(args, srState, {
87
86
  bot,
88
- markdownToHtml: (t) => `<b>${t}</b>`, // stream_reply: bold
89
- escapeMarkdownV2: (t) => t,
90
87
  repairEscapedWhitespace: (t) => t,
91
88
  assertAllowedChat: () => {},
92
89
  resolveThreadId: () => undefined,
93
90
  disableLinkPreview: true,
94
- defaultFormat: 'html',
91
+ // Anything other than the literal 'text' is the rich-markdown path —
92
+ // stream_reply ships the RAW markdown via sendRichMessage (#2669).
93
+ defaultFormat: 'markdown',
95
94
  logStreamingEvent: () => {},
96
95
  historyEnabled: false,
97
96
  recordOutbound: () => {},
@@ -129,12 +128,14 @@ describe('streaming e2e smoke', () => {
129
128
  f.pty.onPartial('drafting…')
130
129
  await microtaskFlush()
131
130
 
131
+ // PTY preview → literal plain sendMessage with the raw terminal text.
132
132
  expect(f.bot.api.sendMessage).toHaveBeenCalledTimes(1)
133
133
  expect(f.bot.api.sendMessage.mock.calls[0][0]).toBe('42')
134
- expect(f.bot.api.sendMessage.mock.calls[0][1]).toBe('<i>drafting…</i>')
134
+ expect(f.bot.api.sendMessage.mock.calls[0][1]).toBe('drafting…')
135
135
 
136
- // Model calls stream_reply with canonical text. Throttle window not
137
- // open yet, so drive timers before awaiting.
136
+ // Model calls stream_reply with canonical text. It REUSES the PTY-created
137
+ // (literalText) stream for this chat+thread, so the edit also takes the
138
+ // literal path (plain string, not a rich { markdown } wrapper).
138
139
  vi.advanceTimersByTime(1000)
139
140
  await microtaskFlush()
140
141
  const p1 = f.fireStreamReply({ chat_id: '42', text: 'final answer' })
@@ -145,7 +146,7 @@ describe('streaming e2e smoke', () => {
145
146
  expect(r1.messageId).toBe(500)
146
147
  expect(f.bot.api.sendMessage).toHaveBeenCalledTimes(1)
147
148
  expect(f.bot.api.editMessageText).toHaveBeenCalledTimes(1)
148
- expect(f.bot.api.editMessageText.mock.calls[0][2]).toBe('<b>final answer</b>')
149
+ expect(f.bot.api.editMessageText.mock.calls[0][2]).toBe('final answer')
149
150
 
150
151
  // done=true with same text → stream.finalize() resolves immediately
151
152
  // (pendingText is null since the edit already landed).
@@ -185,7 +186,8 @@ describe('streaming e2e smoke', () => {
185
186
  await microtaskFlush()
186
187
  await pdone
187
188
 
188
- expect(f.bot.api.sendMessage).toHaveBeenCalledTimes(1)
189
+ // No PTY partial fired → stream_reply created a fresh RICH stream.
190
+ expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
189
191
  const editIds = f.bot.api.editMessageText.mock.calls.map(c => c[1])
190
192
  expect(new Set(editIds).size).toBe(1)
191
193
  expect(f.map.has('1:_')).toBe(false)
@@ -203,7 +205,7 @@ describe('streaming e2e smoke', () => {
203
205
  await microtaskFlush()
204
206
 
205
207
  expect(f.bot.api.sendMessage).toHaveBeenCalledTimes(1)
206
- expect(f.bot.api.sendMessage.mock.calls[0][1]).toBe('<i>early text</i>')
208
+ expect(f.bot.api.sendMessage.mock.calls[0][1]).toBe('early text')
207
209
  expect(f.ptyState.pendingPtyPartial).toBeNull()
208
210
  })
209
211
 
@@ -214,15 +216,18 @@ describe('streaming e2e smoke', () => {
214
216
  const p1 = f.fireStreamReply({ chat_id: '1', text: 'stream_reply first' })
215
217
  await microtaskFlush()
216
218
  await p1
217
- expect(f.bot.api.sendMessage).toHaveBeenCalledTimes(1)
219
+ // stream_reply created a fresh RICH stream (no PTY partial preceded it).
220
+ expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
218
221
 
219
222
  vi.advanceTimersByTime(1000)
220
223
  await microtaskFlush()
221
224
  const action = f.pty.onPartial('stream_reply first — more')
222
225
  await microtaskFlush()
223
226
 
227
+ // The PTY partial reuses the existing stream → an edit, not a new send.
224
228
  expect(action).toBe('update-existing')
225
- expect(f.bot.api.sendMessage).toHaveBeenCalledTimes(1)
229
+ expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
230
+ expect(f.bot.api.sendMessage).not.toHaveBeenCalled()
226
231
  expect(f.bot.api.editMessageText).toHaveBeenCalledTimes(1)
227
232
  expect(f.map.size).toBe(1)
228
233
  })
@@ -240,8 +245,8 @@ describe('streaming e2e smoke', () => {
240
245
  await microtaskFlush()
241
246
  await pB
242
247
 
243
- expect(f.bot.api.sendMessage).toHaveBeenCalledTimes(2)
244
- const chats = f.bot.api.sendMessage.mock.calls.map(c => c[0]).sort()
248
+ expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(2)
249
+ const chats = f.bot.api.sendRichMessage.mock.calls.map(c => c[0]).sort()
245
250
  expect(chats).toEqual(['A', 'B'])
246
251
 
247
252
  vi.advanceTimersByTime(1000)
@@ -278,8 +283,8 @@ describe('streaming e2e smoke', () => {
278
283
  const r = await pending
279
284
 
280
285
  expect(r.status).toBe('updated')
281
- expect(f.bot.api.sendMessage).toHaveBeenCalledTimes(1)
282
- expect(f.bot.api.sendMessage.mock.calls[0][1]).toBe('<b>looking into this…</b>')
286
+ expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
287
+ expect(f.bot.api.sendRichMessage.mock.calls[0][1]).toEqual({ markdown: 'looking into this…' })
283
288
  // PTY slot is claimed because the model is now the answer-lane
284
289
  // surface owner — late PTY partials defer to the model's stream
285
290
  // rather than racing it. (Same suppression pattern as before;
@@ -296,9 +301,9 @@ describe('streaming e2e smoke', () => {
296
301
  const r = await pdone
297
302
 
298
303
  expect(r.status).toBe('finalized')
299
- expect(f.bot.api.sendMessage).toHaveBeenCalledTimes(1)
300
- expect(f.bot.api.sendMessage.mock.calls[0][0]).toBe('1')
301
- expect(f.bot.api.sendMessage.mock.calls[0][1]).toBe('<b>THE ANSWER</b>')
304
+ expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
305
+ expect(f.bot.api.sendRichMessage.mock.calls[0][0]).toBe('1')
306
+ expect(f.bot.api.sendRichMessage.mock.calls[0][1]).toEqual({ markdown: 'THE ANSWER' })
302
307
  expect(f.map.has('1:_')).toBe(false)
303
308
  })
304
309
 
@@ -310,8 +315,8 @@ describe('streaming e2e smoke', () => {
310
315
  await microtaskFlush()
311
316
  await p1
312
317
 
313
- expect(f.bot.api.sendMessage).toHaveBeenCalledTimes(1)
314
- expect(f.bot.api.sendMessage.mock.calls[0][1]).toBe('<b>thinking…</b>')
318
+ expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
319
+ expect(f.bot.api.sendRichMessage.mock.calls[0][1]).toEqual({ markdown: 'thinking…' })
315
320
 
316
321
  vi.advanceTimersByTime(1000)
317
322
  await microtaskFlush()
@@ -320,7 +325,7 @@ describe('streaming e2e smoke', () => {
320
325
  await pdone
321
326
 
322
327
  // First call sent the message; second call edits it (no second send).
323
- expect(f.bot.api.sendMessage).toHaveBeenCalledTimes(1)
328
+ expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
324
329
  expect(f.bot.api.editMessageText).toHaveBeenCalled()
325
330
  })
326
331
 
@@ -340,8 +345,8 @@ describe('streaming e2e smoke', () => {
340
345
  const r = await p
341
346
 
342
347
  expect(r.status).toBe('updated')
343
- expect(f.bot.api.sendMessage).toHaveBeenCalledTimes(1)
344
- expect(f.bot.api.sendMessage.mock.calls[0][1]).toBe('<b>Plan → Run → Done</b>')
348
+ expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
349
+ expect(f.bot.api.sendRichMessage.mock.calls[0][1]).toEqual({ markdown: 'Plan → Run → Done' })
345
350
  expect(f.map.has('1:_:progress')).toBe(true)
346
351
  })
347
352
 
@@ -353,8 +358,8 @@ describe('streaming e2e smoke', () => {
353
358
  await microtaskFlush()
354
359
  await p1
355
360
 
356
- expect(f.bot.api.sendMessage).toHaveBeenCalledTimes(1)
357
- expect(f.bot.api.sendMessage.mock.calls[0][1]).toBe('<b>chunk 1</b>')
361
+ expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
362
+ expect(f.bot.api.sendRichMessage.mock.calls[0][1]).toEqual({ markdown: 'chunk 1' })
358
363
 
359
364
  vi.advanceTimersByTime(1000)
360
365
  await microtaskFlush()
@@ -381,7 +386,7 @@ describe('streaming e2e smoke', () => {
381
386
  await pdone
382
387
 
383
388
  const lastEdit = f.bot.api.editMessageText.mock.calls.at(-1)
384
- expect(lastEdit?.[2]).toBe('<b>THE ACTUAL ANSWER</b>')
389
+ expect(lastEdit?.[2]).toEqual({ markdown: 'THE ACTUAL ANSWER' })
385
390
  expect(f.map.has('1:_')).toBe(false)
386
391
  })
387
392
  })
@@ -37,7 +37,8 @@ function modelStreamReply(params: {
37
37
  text: string
38
38
  done?: boolean
39
39
  throttleMs?: number
40
- parseMode?: 'HTML' | 'MarkdownV2'
40
+ /** Literal plain-text stream (no rich-markdown parser). Default: rich. */
41
+ literalText?: boolean
41
42
  }): { stream: DraftStreamHandle; settled: Promise<void> } {
42
43
  const key = streamKey(params.chatId, params.threadId)
43
44
  let stream = params.map.get(key)
@@ -46,7 +47,7 @@ function modelStreamReply(params: {
46
47
  bot: params.bot,
47
48
  chatId: params.chatId,
48
49
  threadId: params.threadId,
49
- parseMode: params.parseMode,
50
+ literalText: params.literalText,
50
51
  throttleMs: params.throttleMs ?? 600,
51
52
  })
52
53
  params.map.set(key, stream)
@@ -79,7 +80,7 @@ describe('streaming orchestration — activeDraftStreams map', () => {
79
80
 
80
81
  expect(a).toBe(b)
81
82
  expect(map.size).toBe(1)
82
- expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
83
+ expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
83
84
  expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
84
85
  })
85
86
 
@@ -96,7 +97,7 @@ describe('streaming orchestration — activeDraftStreams map', () => {
96
97
  await microtaskFlush()
97
98
 
98
99
  expect(first).not.toBe(second)
99
- expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
100
+ expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(2)
100
101
  expect(second.getMessageId()).toBe(501)
101
102
  })
102
103
 
@@ -108,8 +109,8 @@ describe('streaming orchestration — activeDraftStreams map', () => {
108
109
  await microtaskFlush()
109
110
 
110
111
  expect(map.size).toBe(2)
111
- expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
112
- const chatIds = bot.api.sendMessage.mock.calls.map(c => c[0]).sort()
112
+ expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(2)
113
+ const chatIds = bot.api.sendRichMessage.mock.calls.map(c => c[0]).sort()
113
114
  expect(chatIds).toEqual(['1', '2'])
114
115
  })
115
116
 
@@ -121,32 +122,32 @@ describe('streaming orchestration — activeDraftStreams map', () => {
121
122
  await microtaskFlush()
122
123
 
123
124
  expect(map.size).toBe(2)
124
- expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
125
- const threads = bot.api.sendMessage.mock.calls.map(c => c[2]?.message_thread_id).sort()
125
+ expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(2)
126
+ const threads = bot.api.sendRichMessage.mock.calls.map(c => c[2]?.message_thread_id).sort()
126
127
  expect(threads).toEqual([10, 20])
127
128
  })
128
129
 
129
130
  it('pty preview → stream_reply handoff: second call edits the PTY message in place', async () => {
130
131
  const map = new Map<string, DraftStreamHandle>()
131
132
 
132
- const ptyStream = createStreamController({ bot, chatId: '1', parseMode: 'HTML', throttleMs: 600 })
133
+ const ptyStream = createStreamController({ bot, chatId: '1', throttleMs: 600 })
133
134
  map.set('1:_', ptyStream)
134
- void ptyStream.update('<i>drafting…</i>')
135
+ void ptyStream.update('_drafting…_')
135
136
  await microtaskFlush()
136
137
 
137
- expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
138
+ expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
138
139
  const previewId = ptyStream.getMessageId()
139
140
  expect(previewId).toBe(500)
140
141
 
141
142
  vi.advanceTimersByTime(600)
142
143
  await microtaskFlush()
143
144
  const { settled } = modelStreamReply({
144
- bot, map, chatId: '1', parseMode: 'HTML', text: 'final answer', done: true,
145
+ bot, map, chatId: '1', text: 'final answer', done: true,
145
146
  })
146
147
  await microtaskFlush()
147
148
  await settled
148
149
 
149
- expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
150
+ expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
150
151
  expect(bot.api.editMessageText).toHaveBeenCalled()
151
152
  expect(bot.api.editMessageText.mock.calls[0][1]).toBe(previewId)
152
153
  expect(map.has('1:_')).toBe(false)
@@ -165,10 +166,10 @@ describe('streaming orchestration — activeDraftStreams map', () => {
165
166
  }
166
167
  await microtaskFlush()
167
168
 
168
- expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
169
- expect(bot.api.sendMessage.mock.calls[0][1]).toBe('one')
169
+ expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
170
+ expect((bot.api.sendRichMessage.mock.calls[0][1] as { markdown: string }).markdown).toBe('one')
170
171
  expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
171
- expect(bot.api.editMessageText.mock.calls[0][2]).toBe('four')
172
+ expect((bot.api.editMessageText.mock.calls[0][2] as { markdown: string }).markdown).toBe('four')
172
173
 
173
174
  // No further work scheduled; advancing timers should be a no-op.
174
175
  vi.advanceTimersByTime(5000)
@@ -188,7 +189,7 @@ describe('streaming orchestration — activeDraftStreams map', () => {
188
189
  await settled
189
190
 
190
191
  expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
191
- expect(bot.api.editMessageText.mock.calls[0][2]).toBe('final answer')
192
+ expect((bot.api.editMessageText.mock.calls[0][2] as { markdown: string }).markdown).toBe('final answer')
192
193
  expect(map.size).toBe(0)
193
194
  })
194
195
 
@@ -197,7 +198,7 @@ describe('streaming orchestration — activeDraftStreams map', () => {
197
198
  // messageId=null. A subsequent update() triggers a fresh send on the
198
199
  // same stream object — this is what makes a momentary network blip
199
200
  // recoverable without the caller needing to evict-and-recreate.
200
- bot.api.sendMessage
201
+ bot.api.sendRichMessage
201
202
  .mockImplementationOnce(async () => { throw new Error('network down') })
202
203
  .mockImplementationOnce(async () => ({ message_id: 777 }))
203
204
 
@@ -206,14 +207,14 @@ describe('streaming orchestration — activeDraftStreams map', () => {
206
207
  await microtaskFlush()
207
208
 
208
209
  expect(stream.getMessageId()).toBeNull()
209
- expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
210
+ expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
210
211
 
211
212
  // Caller issues next snapshot — should retry send, not try to edit null.
212
213
  vi.advanceTimersByTime(1000)
213
214
  modelStreamReply({ bot, map, chatId: '1', text: 'second try' })
214
215
  await microtaskFlush()
215
216
 
216
- expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
217
+ expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(2)
217
218
  expect(bot.api.editMessageText).not.toHaveBeenCalled()
218
219
  expect(stream.getMessageId()).toBe(777)
219
220
  })
@@ -249,7 +250,9 @@ function modelPtyPartial(params: {
249
250
  bot: params.bot,
250
251
  chatId: params.chatId,
251
252
  threadId: params.threadId,
252
- parseMode: 'HTML',
253
+ // PTY-tail previews ship as literal plain text (#2669) — the raw
254
+ // TUI capture is not markdown.
255
+ literalText: true,
253
256
  throttleMs: params.throttleMs ?? 600,
254
257
  })
255
258
  params.map.set(key, stream)
@@ -404,8 +407,6 @@ function makeActivityDeps(
404
407
  ): StreamReplyDeps {
405
408
  return {
406
409
  bot,
407
- markdownToHtml: (t) => t,
408
- escapeMarkdownV2: (t) => t,
409
410
  repairEscapedWhitespace: (t) => t,
410
411
  assertAllowedChat: () => {},
411
412
  resolveThreadId: (_, explicit) => (explicit != null ? Number(explicit) : undefined),
@@ -706,7 +707,7 @@ describe('PTY activity-lane wiring (onActivity → stream_reply lane="activity")
706
707
  const deps = makeActivityDeps(bot)
707
708
 
708
709
  await handleStreamReply(
709
- { chat_id: '1', text: '⚙️ Working…', lane: 'progress', format: 'html' },
710
+ { chat_id: '1', text: '⚙️ Working…', lane: 'progress', format: 'markdown' },
710
711
  state, deps,
711
712
  )
712
713
  await microtaskFlush()
@@ -726,12 +727,12 @@ describe('PTY activity-lane wiring (onActivity → stream_reply lane="activity")
726
727
 
727
728
  // Turn 1: progress card lives at some messageId.
728
729
  await handleStreamReply(
729
- { chat_id: '1', text: '⚙️ Working… (turn 1)', lane: 'progress', format: 'html' },
730
+ { chat_id: '1', text: '⚙️ Working… (turn 1)', lane: 'progress', format: 'markdown' },
730
731
  state, deps,
731
732
  )
732
733
  await microtaskFlush()
733
734
  const firstProgressId = state.activeDraftStreams.get('1:_:progress')!.getMessageId()
734
- const sendsBefore = bot.api.sendMessage.mock.calls.length
735
+ const sendsBefore = bot.api.sendRichMessage.mock.calls.length
735
736
 
736
737
  // turn_end closes the progress lane.
737
738
  await modelCloseProgressLane(state, '1', undefined)
@@ -741,7 +742,7 @@ describe('PTY activity-lane wiring (onActivity → stream_reply lane="activity")
741
742
  // edit the previous one — this is what prevents the shrink-flicker
742
743
  // described in the bug report.
743
744
  await handleStreamReply(
744
- { chat_id: '1', text: '⚙️ Working… (turn 2)', lane: 'progress', format: 'html' },
745
+ { chat_id: '1', text: '⚙️ Working… (turn 2)', lane: 'progress', format: 'markdown' },
745
746
  state, deps,
746
747
  )
747
748
  await microtaskFlush()
@@ -749,6 +750,6 @@ describe('PTY activity-lane wiring (onActivity → stream_reply lane="activity")
749
750
  expect(state.activeDraftStreams.has('1:_:progress')).toBe(true)
750
751
  const secondProgressId = state.activeDraftStreams.get('1:_:progress')!.getMessageId()
751
752
  expect(secondProgressId).not.toBe(firstProgressId)
752
- expect(bot.api.sendMessage.mock.calls.length).toBe(sendsBefore + 1)
753
+ expect(bot.api.sendRichMessage.mock.calls.length).toBe(sendsBefore + 1)
753
754
  })
754
755
  })