@otto-assistant/bridge 0.4.92

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 (483) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,293 @@
1
+ // E2e tests for abort, model-switch, and retry scenarios.
2
+ // Split from thread-queue-advanced.e2e.test.ts for parallelization.
3
+ import { describe, test, expect } from 'vitest';
4
+ import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
5
+ import { getRuntime, } from './session-handler/thread-session-runtime.js';
6
+ import { getThreadState } from './session-handler/thread-runtime-state.js';
7
+ import { setSessionModel } from './database.js';
8
+ import { waitForFooterMessage, waitForBotMessageContaining, waitForBotReplyAfterUserMessage, } from './test-utils.js';
9
+ const TEXT_CHANNEL_ID = '200000000000001003';
10
+ const e2eTest = describe;
11
+ e2eTest('queue advanced: abort and retry', () => {
12
+ const ctx = setupQueueAdvancedSuite({
13
+ channelId: TEXT_CHANNEL_ID,
14
+ channelName: 'qa-abort-e2e',
15
+ dirName: 'qa-abort-e2e',
16
+ username: 'queue-advanced-tester',
17
+ });
18
+ test('slow tool call (sleep) gets aborted by explicit abort, then queue continues', async () => {
19
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
20
+ content: 'Reply with exactly: oscar',
21
+ });
22
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
23
+ timeout: 4_000,
24
+ predicate: (t) => {
25
+ return t.name === 'Reply with exactly: oscar';
26
+ },
27
+ });
28
+ const th = ctx.discord.thread(thread.id);
29
+ const firstReply = await th.waitForBotReply({ timeout: 4_000 });
30
+ expect(firstReply.content.trim().length).toBeGreaterThan(0);
31
+ // Wait for the first completion footer so it lands in a deterministic position
32
+ await waitForFooterMessage({
33
+ discord: ctx.discord,
34
+ threadId: thread.id,
35
+ timeout: 4_000,
36
+ });
37
+ const before = await th.getMessages();
38
+ const beforeBotCount = before.filter((m) => {
39
+ return m.author.id === ctx.discord.botUserId;
40
+ }).length;
41
+ await th.user(TEST_USER_ID).sendMessage({
42
+ content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
43
+ });
44
+ // The matcher emits "starting sleep 100" text before the long delay.
45
+ // Wait for it to land in Discord BEFORE aborting so the message is in a
46
+ // deterministic position and the abort produces no further stray messages.
47
+ await waitForBotMessageContaining({
48
+ discord: ctx.discord,
49
+ threadId: thread.id,
50
+ userId: TEST_USER_ID,
51
+ text: 'starting sleep',
52
+ afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
53
+ timeout: 4_000,
54
+ });
55
+ const runtime = getRuntime(thread.id);
56
+ expect(runtime).toBeDefined();
57
+ if (!runtime) {
58
+ throw new Error('Expected runtime to exist for explicit-abort test');
59
+ }
60
+ runtime.abortActiveRun('test-explicit-abort');
61
+ await th.user(TEST_USER_ID).sendMessage({
62
+ content: 'Reply with exactly: papa',
63
+ });
64
+ const after = await waitForBotReplyAfterUserMessage({
65
+ discord: ctx.discord,
66
+ threadId: thread.id,
67
+ userId: TEST_USER_ID,
68
+ userMessageIncludes: 'papa',
69
+ timeout: 8_000,
70
+ });
71
+ const afterBotMessages = after.filter((m) => {
72
+ return m.author.id === ctx.discord.botUserId;
73
+ });
74
+ await waitForFooterMessage({
75
+ discord: ctx.discord,
76
+ threadId: thread.id,
77
+ timeout: 8_000,
78
+ afterMessageIncludes: 'papa',
79
+ afterAuthorId: TEST_USER_ID,
80
+ });
81
+ // Assert ordering invariants instead of exact snapshot — the papa reply
82
+ // and footer can interleave non-deterministically.
83
+ const timeline = await th.text();
84
+ expect(timeline).toContain('Reply with exactly: oscar');
85
+ expect(timeline).toContain('PLUGIN_TIMEOUT_SLEEP_MARKER');
86
+ expect(timeline).toContain('⬥ starting sleep 100');
87
+ expect(timeline).toContain('Reply with exactly: papa');
88
+ expect(timeline).toContain('*project ⋅ main ⋅');
89
+ // oscar comes before the sleep marker, sleep before papa
90
+ const oscarIdx = timeline.indexOf('oscar');
91
+ const sleepIdx = timeline.indexOf('PLUGIN_TIMEOUT_SLEEP_MARKER');
92
+ const papaIdx = timeline.indexOf('papa');
93
+ expect(oscarIdx).toBeLessThan(sleepIdx);
94
+ expect(sleepIdx).toBeLessThan(papaIdx);
95
+ expect(afterBotMessages.length).toBeGreaterThanOrEqual(beforeBotCount + 1);
96
+ const sleepToolIndex = after.findIndex((m) => {
97
+ return (m.author.id === TEST_USER_ID &&
98
+ m.content.includes('PLUGIN_TIMEOUT_SLEEP_MARKER'));
99
+ });
100
+ expect(sleepToolIndex).toBeGreaterThan(-1);
101
+ const userPapaIndex = after.findIndex((m) => {
102
+ return m.author.id === TEST_USER_ID && m.content.includes('papa');
103
+ });
104
+ expect(userPapaIndex).toBeGreaterThan(-1);
105
+ expect(sleepToolIndex).toBeLessThan(userPapaIndex);
106
+ const lastBotIndex = after.findLastIndex((m) => {
107
+ return m.author.id === ctx.discord.botUserId;
108
+ });
109
+ expect(userPapaIndex).toBeLessThan(lastBotIndex);
110
+ }, 12_000);
111
+ test('explicit abort emits MessageAbortedError and does not emit footer', async () => {
112
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
113
+ content: 'Reply with exactly: abort-no-footer-setup',
114
+ });
115
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
116
+ timeout: 4_000,
117
+ predicate: (t) => {
118
+ return t.name === 'Reply with exactly: abort-no-footer-setup';
119
+ },
120
+ });
121
+ const th = ctx.discord.thread(thread.id);
122
+ await th.waitForBotReply({ timeout: 4_000 });
123
+ await waitForBotMessageContaining({
124
+ discord: ctx.discord,
125
+ threadId: thread.id,
126
+ userId: TEST_USER_ID,
127
+ text: '⋅',
128
+ timeout: 4_000,
129
+ });
130
+ await th.user(TEST_USER_ID).sendMessage({
131
+ content: 'SLOW_ABORT_MARKER run long response',
132
+ });
133
+ const runtime = getRuntime(thread.id);
134
+ expect(runtime).toBeDefined();
135
+ if (!runtime) {
136
+ throw new Error('Expected runtime to exist for abort no-footer test');
137
+ }
138
+ const beforeAbortMessages = await th.getMessages();
139
+ const baselineCount = beforeAbortMessages.length;
140
+ runtime.abortActiveRun('test-no-footer-on-abort');
141
+ for (let i = 0; i < 10; i++) {
142
+ await new Promise((resolve) => {
143
+ setTimeout(resolve, 20);
144
+ });
145
+ const msgs = await th.getMessages();
146
+ const newMsgs = msgs.slice(baselineCount);
147
+ const hasFooter = newMsgs.some((m) => {
148
+ return m.author.id === ctx.discord.botUserId
149
+ && m.content.startsWith('*')
150
+ && m.content.includes('⋅');
151
+ });
152
+ expect(hasFooter).toBe(false);
153
+ }
154
+ expect(await th.text()).toMatchInlineSnapshot(`
155
+ "--- from: user (queue-advanced-tester)
156
+ Reply with exactly: abort-no-footer-setup
157
+ --- from: assistant (TestBot)
158
+ ⬥ ok
159
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
160
+ --- from: user (queue-advanced-tester)
161
+ SLOW_ABORT_MARKER run long response"
162
+ `);
163
+ }, 10_000);
164
+ test.skip('explicit abort stale-idle window: follow-up prompt still gets assistant text', async () => {
165
+ const setupPrompt = 'Reply with exactly: race-setup-1';
166
+ const raceFinalPrompt = 'Reply with exactly: race-final-1';
167
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
168
+ content: setupPrompt,
169
+ });
170
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
171
+ timeout: 4_000,
172
+ predicate: (t) => {
173
+ return t.name === setupPrompt;
174
+ },
175
+ });
176
+ const th = ctx.discord.thread(thread.id);
177
+ const setupReply = await th.waitForBotReply({ timeout: 4_000 });
178
+ expect(setupReply.content.trim().length).toBeGreaterThan(0);
179
+ await th.user(TEST_USER_ID).sendMessage({
180
+ content: 'SLOW_ABORT_MARKER run long response',
181
+ });
182
+ const runtime = getRuntime(thread.id);
183
+ expect(runtime).toBeDefined();
184
+ if (!runtime) {
185
+ throw new Error('Expected runtime to exist for race abort scenario');
186
+ }
187
+ runtime.abortActiveRun('test-race-abort');
188
+ await th.user(TEST_USER_ID).sendMessage({
189
+ content: raceFinalPrompt,
190
+ });
191
+ await waitForBotReplyAfterUserMessage({
192
+ discord: ctx.discord,
193
+ threadId: thread.id,
194
+ userId: TEST_USER_ID,
195
+ userMessageIncludes: raceFinalPrompt,
196
+ timeout: 4_000,
197
+ });
198
+ }, 8_000);
199
+ test('model switch mid-session aborts and restarts from same session history', async () => {
200
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
201
+ content: 'Reply with exactly: retry-setup',
202
+ });
203
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
204
+ timeout: 4_000,
205
+ predicate: (t) => {
206
+ return t.name === 'Reply with exactly: retry-setup';
207
+ },
208
+ });
209
+ const th = ctx.discord.thread(thread.id);
210
+ const firstReply = await th.waitForBotReply({ timeout: 4_000 });
211
+ expect(firstReply.content.trim().length).toBeGreaterThan(0);
212
+ await th.user(TEST_USER_ID).sendMessage({
213
+ content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
214
+ });
215
+ await waitForBotMessageContaining({
216
+ discord: ctx.discord,
217
+ threadId: thread.id,
218
+ userId: TEST_USER_ID,
219
+ text: 'starting sleep',
220
+ afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
221
+ timeout: 4_000,
222
+ });
223
+ const sessionId = getThreadState(thread.id)?.sessionId;
224
+ expect(sessionId).toBeDefined();
225
+ if (!sessionId) {
226
+ throw new Error('Expected active session id for model switch test');
227
+ }
228
+ await setSessionModel({
229
+ sessionId,
230
+ modelId: 'deterministic-provider/deterministic-v3',
231
+ variant: null,
232
+ });
233
+ const runtime = getRuntime(thread.id);
234
+ expect(runtime).toBeDefined();
235
+ if (!runtime) {
236
+ throw new Error('Expected runtime to exist for model switch test');
237
+ }
238
+ const retried = await runtime.retryLastUserPrompt();
239
+ expect(retried).toBe(true);
240
+ await th.user(TEST_USER_ID).sendMessage({
241
+ content: 'Reply with exactly: model-switch-followup',
242
+ });
243
+ await waitForBotReplyAfterUserMessage({
244
+ discord: ctx.discord,
245
+ threadId: thread.id,
246
+ userId: TEST_USER_ID,
247
+ userMessageIncludes: 'model-switch-followup',
248
+ timeout: 4_000,
249
+ });
250
+ // Wait for potential footer to arrive (race between step-finish interrupt
251
+ // and model switch settling means footer may or may not appear).
252
+ await new Promise((resolve) => {
253
+ setTimeout(resolve, 200);
254
+ });
255
+ const text = await th.text();
256
+ // The follow-up reply ("ok") must be present with deterministic-v3
257
+ expect(text).toContain('Reply with exactly: model-switch-followup');
258
+ expect(text).toContain('⬥ ok');
259
+ // The old sleep text should be visible from the first turn
260
+ expect(text).toContain('starting sleep 100');
261
+ }, 10_000);
262
+ test('abortActiveRun settles correctly during long-running request', async () => {
263
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
264
+ content: 'Reply with exactly: force-abort-setup',
265
+ });
266
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
267
+ timeout: 4_000,
268
+ predicate: (t) => {
269
+ return t.name === 'Reply with exactly: force-abort-setup';
270
+ },
271
+ });
272
+ const th = ctx.discord.thread(thread.id);
273
+ const setupReply = await th.waitForBotReply({ timeout: 4_000 });
274
+ expect(setupReply.content.trim().length).toBeGreaterThan(0);
275
+ await th.user(TEST_USER_ID).sendMessage({
276
+ content: 'SLOW_ABORT_MARKER run long response',
277
+ });
278
+ const runtime = getRuntime(thread.id);
279
+ expect(runtime).toBeDefined();
280
+ if (!runtime) {
281
+ throw new Error('Expected runtime to exist for forced-abort test');
282
+ }
283
+ runtime.abortActiveRun('force-abort-test');
284
+ expect(await th.text()).toMatchInlineSnapshot(`
285
+ "--- from: user (queue-advanced-tester)
286
+ Reply with exactly: force-abort-setup
287
+ --- from: assistant (TestBot)
288
+ ⬥ ok
289
+ --- from: user (queue-advanced-tester)
290
+ SLOW_ABORT_MARKER run long response"
291
+ `);
292
+ }, 10_000);
293
+ });
@@ -0,0 +1,206 @@
1
+ // E2e regression test for action button click continuation in thread sessions.
2
+ // Reproduces the bug where button click interaction acks but the session does not continue.
3
+ import { describe, test, expect } from 'vitest';
4
+ import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
5
+ import { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
6
+ import { getThreadSession } from './database.js';
7
+ import { pendingActionButtonContexts, showActionButtons, } from './commands/action-buttons.js';
8
+ const TEXT_CHANNEL_ID = '200000000000001006';
9
+ async function waitForPendingActionButtons({ threadId, timeoutMs, }) {
10
+ const start = Date.now();
11
+ while (Date.now() - start < timeoutMs) {
12
+ const entry = [...pendingActionButtonContexts.entries()].find(([, context]) => {
13
+ return context.thread.id === threadId && Boolean(context.messageId);
14
+ });
15
+ if (entry) {
16
+ const [contextHash, context] = entry;
17
+ if (context.messageId) {
18
+ return { contextHash, messageId: context.messageId };
19
+ }
20
+ }
21
+ await new Promise((resolve) => {
22
+ setTimeout(resolve, 100);
23
+ });
24
+ }
25
+ throw new Error('Timed out waiting for pending action buttons context');
26
+ }
27
+ async function waitForNoPendingActionButtons({ threadId, timeoutMs, }) {
28
+ const start = Date.now();
29
+ while (Date.now() - start < timeoutMs) {
30
+ const stillPending = [...pendingActionButtonContexts.values()].some((context) => {
31
+ return context.thread.id === threadId;
32
+ });
33
+ if (!stillPending) {
34
+ return;
35
+ }
36
+ await new Promise((resolve) => {
37
+ setTimeout(resolve, 100);
38
+ });
39
+ }
40
+ throw new Error('Timed out waiting for action buttons cleanup');
41
+ }
42
+ describe('queue advanced: action buttons', () => {
43
+ const ctx = setupQueueAdvancedSuite({
44
+ channelId: TEXT_CHANNEL_ID,
45
+ channelName: 'qa-action-buttons-e2e',
46
+ dirName: 'qa-action-buttons-e2e',
47
+ username: 'queue-action-tester',
48
+ });
49
+ test('button click should continue the session with a follow-up assistant reply', async () => {
50
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
51
+ content: 'Reply with exactly: action-button-setup',
52
+ });
53
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
54
+ timeout: 4_000,
55
+ predicate: (t) => {
56
+ return t.name === 'Reply with exactly: action-button-setup';
57
+ },
58
+ });
59
+ const th = ctx.discord.thread(thread.id);
60
+ await waitForBotMessageContaining({
61
+ discord: ctx.discord,
62
+ threadId: thread.id,
63
+ userId: TEST_USER_ID,
64
+ text: 'ok',
65
+ timeout: 4_000,
66
+ });
67
+ await waitForFooterMessage({
68
+ discord: ctx.discord,
69
+ threadId: thread.id,
70
+ timeout: 4_000,
71
+ afterMessageIncludes: 'ok',
72
+ afterAuthorId: ctx.discord.botUserId,
73
+ });
74
+ const currentSessionId = await getThreadSession(thread.id);
75
+ if (!currentSessionId) {
76
+ throw new Error('Expected thread session id before showing action buttons');
77
+ }
78
+ const channel = await ctx.botClient.channels.fetch(thread.id);
79
+ if (!channel || !channel.isThread()) {
80
+ throw new Error('Expected Discord thread channel for action button test');
81
+ }
82
+ await showActionButtons({
83
+ thread: channel,
84
+ sessionId: currentSessionId,
85
+ directory: ctx.directories.projectDirectory,
86
+ buttons: [{ label: 'Continue action-buttons flow', color: 'green' }],
87
+ });
88
+ const action = await waitForPendingActionButtons({
89
+ threadId: thread.id,
90
+ timeoutMs: 12_000,
91
+ });
92
+ await waitForBotMessageContaining({
93
+ discord: ctx.discord,
94
+ threadId: thread.id,
95
+ userId: TEST_USER_ID,
96
+ text: 'Action Required',
97
+ timeout: 12_000,
98
+ });
99
+ const interaction = await th.user(TEST_USER_ID).clickButton({
100
+ messageId: action.messageId,
101
+ customId: `action_button:${action.contextHash}:0`,
102
+ });
103
+ await th.waitForInteractionAck({
104
+ interactionId: interaction.id,
105
+ timeout: 4_000,
106
+ });
107
+ await waitForBotMessageContaining({
108
+ discord: ctx.discord,
109
+ threadId: thread.id,
110
+ text: 'action-buttons-click-continued',
111
+ timeout: 12_000,
112
+ });
113
+ await waitForFooterMessage({
114
+ discord: ctx.discord,
115
+ threadId: thread.id,
116
+ timeout: 12_000,
117
+ afterMessageIncludes: 'action-buttons-click-continued',
118
+ afterAuthorId: ctx.discord.botUserId,
119
+ });
120
+ const timeline = await th.text({ showInteractions: true });
121
+ expect(timeline).toMatchInlineSnapshot(`
122
+ "--- from: user (queue-action-tester)
123
+ Reply with exactly: action-button-setup
124
+ --- from: assistant (TestBot)
125
+ ⬥ ok
126
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
127
+ **Action Required**
128
+ _Selected: Continue action-buttons flow_
129
+ [user clicks button]
130
+ ⬥ action-buttons-click-continued
131
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
132
+ `);
133
+ expect(timeline).toContain('action-buttons-click-continued');
134
+ }, 20_000);
135
+ test('manual thread message dismisses pending action buttons', async () => {
136
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
137
+ content: 'Reply with exactly: action-button-dismiss-setup',
138
+ });
139
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
140
+ timeout: 4_000,
141
+ predicate: (t) => {
142
+ return t.name === 'Reply with exactly: action-button-dismiss-setup';
143
+ },
144
+ });
145
+ const th = ctx.discord.thread(thread.id);
146
+ await waitForBotMessageContaining({
147
+ discord: ctx.discord,
148
+ threadId: thread.id,
149
+ userId: TEST_USER_ID,
150
+ text: 'ok',
151
+ timeout: 4_000,
152
+ });
153
+ await waitForFooterMessage({
154
+ discord: ctx.discord,
155
+ threadId: thread.id,
156
+ timeout: 4_000,
157
+ afterMessageIncludes: 'ok',
158
+ afterAuthorId: ctx.discord.botUserId,
159
+ });
160
+ const currentSessionId = await getThreadSession(thread.id);
161
+ if (!currentSessionId) {
162
+ throw new Error('Expected thread session id before showing action buttons');
163
+ }
164
+ const channel = await ctx.botClient.channels.fetch(thread.id);
165
+ if (!channel || !channel.isThread()) {
166
+ throw new Error('Expected Discord thread channel for action button test');
167
+ }
168
+ await showActionButtons({
169
+ thread: channel,
170
+ sessionId: currentSessionId,
171
+ directory: ctx.directories.projectDirectory,
172
+ buttons: [{ label: 'Dismiss me', color: 'white' }],
173
+ });
174
+ await waitForPendingActionButtons({
175
+ threadId: thread.id,
176
+ timeoutMs: 4_000,
177
+ });
178
+ await th.user(TEST_USER_ID).sendMessage({
179
+ content: 'Reply with exactly: post-dismiss-user-message',
180
+ });
181
+ await waitForBotMessageContaining({
182
+ discord: ctx.discord,
183
+ threadId: thread.id,
184
+ text: 'Buttons dismissed.',
185
+ timeout: 4_000,
186
+ });
187
+ await waitForNoPendingActionButtons({
188
+ threadId: thread.id,
189
+ timeoutMs: 4_000,
190
+ });
191
+ const timeline = await th.text({ showInteractions: true });
192
+ expect(timeline).toMatchInlineSnapshot(`
193
+ "--- from: user (queue-action-tester)
194
+ Reply with exactly: action-button-dismiss-setup
195
+ --- from: assistant (TestBot)
196
+ ⬥ ok
197
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
198
+ **Action Required**
199
+ _Buttons dismissed._
200
+ --- from: user (queue-action-tester)
201
+ Reply with exactly: post-dismiss-user-message"
202
+ `);
203
+ expect(timeline).toContain('_Buttons dismissed._');
204
+ expect(timeline).toContain('post-dismiss-user-message');
205
+ }, 20_000);
206
+ });