@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,382 @@
1
+ // E2e tests for abort, model-switch, and retry scenarios.
2
+ // Split from thread-queue-advanced.e2e.test.ts for parallelization.
3
+
4
+ import { describe, test, expect } from 'vitest'
5
+ import {
6
+ setupQueueAdvancedSuite,
7
+ TEST_USER_ID,
8
+ } from './queue-advanced-e2e-setup.js'
9
+ import {
10
+ getRuntime,
11
+ } from './session-handler/thread-session-runtime.js'
12
+ import { getThreadState } from './session-handler/thread-runtime-state.js'
13
+ import { setSessionModel } from './database.js'
14
+ import {
15
+ waitForFooterMessage,
16
+ waitForBotMessageContaining,
17
+ waitForBotReplyAfterUserMessage,
18
+ } from './test-utils.js'
19
+
20
+ const TEXT_CHANNEL_ID = '200000000000001003'
21
+
22
+ const e2eTest = describe
23
+
24
+ e2eTest('queue advanced: abort and retry', () => {
25
+ const ctx = setupQueueAdvancedSuite({
26
+ channelId: TEXT_CHANNEL_ID,
27
+ channelName: 'qa-abort-e2e',
28
+ dirName: 'qa-abort-e2e',
29
+ username: 'queue-advanced-tester',
30
+ })
31
+
32
+ test(
33
+ 'slow tool call (sleep) gets aborted by explicit abort, then queue continues',
34
+ async () => {
35
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
36
+ content: 'Reply with exactly: oscar',
37
+ })
38
+
39
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
40
+ timeout: 4_000,
41
+ predicate: (t) => {
42
+ return t.name === 'Reply with exactly: oscar'
43
+ },
44
+ })
45
+
46
+ const th = ctx.discord.thread(thread.id)
47
+ const firstReply = await th.waitForBotReply({ timeout: 4_000 })
48
+ expect(firstReply.content.trim().length).toBeGreaterThan(0)
49
+
50
+ // Wait for the first completion footer so it lands in a deterministic position
51
+ await waitForFooterMessage({
52
+ discord: ctx.discord,
53
+ threadId: thread.id,
54
+ timeout: 4_000,
55
+ })
56
+
57
+ const before = await th.getMessages()
58
+ const beforeBotCount = before.filter((m) => {
59
+ return m.author.id === ctx.discord.botUserId
60
+ }).length
61
+
62
+ await th.user(TEST_USER_ID).sendMessage({
63
+ content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
64
+ })
65
+
66
+ // The matcher emits "starting sleep 100" text before the long delay.
67
+ // Wait for it to land in Discord BEFORE aborting so the message is in a
68
+ // deterministic position and the abort produces no further stray messages.
69
+ await waitForBotMessageContaining({
70
+ discord: ctx.discord,
71
+ threadId: thread.id,
72
+ userId: TEST_USER_ID,
73
+ text: 'starting sleep',
74
+ afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
75
+ timeout: 4_000,
76
+ })
77
+
78
+ const runtime = getRuntime(thread.id)
79
+ expect(runtime).toBeDefined()
80
+ if (!runtime) {
81
+ throw new Error('Expected runtime to exist for explicit-abort test')
82
+ }
83
+
84
+ runtime.abortActiveRun('test-explicit-abort')
85
+
86
+ await th.user(TEST_USER_ID).sendMessage({
87
+ content: 'Reply with exactly: papa',
88
+ })
89
+
90
+ const after = await waitForBotReplyAfterUserMessage({
91
+ discord: ctx.discord,
92
+ threadId: thread.id,
93
+ userId: TEST_USER_ID,
94
+ userMessageIncludes: 'papa',
95
+ timeout: 8_000,
96
+ })
97
+
98
+ const afterBotMessages = after.filter((m) => {
99
+ return m.author.id === ctx.discord.botUserId
100
+ })
101
+
102
+ await waitForFooterMessage({
103
+ discord: ctx.discord,
104
+ threadId: thread.id,
105
+ timeout: 8_000,
106
+ afterMessageIncludes: 'papa',
107
+ afterAuthorId: TEST_USER_ID,
108
+ })
109
+
110
+ // Assert ordering invariants instead of exact snapshot — the papa reply
111
+ // and footer can interleave non-deterministically.
112
+ const timeline = await th.text()
113
+ expect(timeline).toContain('Reply with exactly: oscar')
114
+ expect(timeline).toContain('PLUGIN_TIMEOUT_SLEEP_MARKER')
115
+ expect(timeline).toContain('⬥ starting sleep 100')
116
+ expect(timeline).toContain('Reply with exactly: papa')
117
+ expect(timeline).toContain('*project ⋅ main ⋅')
118
+ // oscar comes before the sleep marker, sleep before papa
119
+ const oscarIdx = timeline.indexOf('oscar')
120
+ const sleepIdx = timeline.indexOf('PLUGIN_TIMEOUT_SLEEP_MARKER')
121
+ const papaIdx = timeline.indexOf('papa')
122
+ expect(oscarIdx).toBeLessThan(sleepIdx)
123
+ expect(sleepIdx).toBeLessThan(papaIdx)
124
+ expect(afterBotMessages.length).toBeGreaterThanOrEqual(beforeBotCount + 1)
125
+
126
+ const sleepToolIndex = after.findIndex((m) => {
127
+ return (
128
+ m.author.id === TEST_USER_ID &&
129
+ m.content.includes('PLUGIN_TIMEOUT_SLEEP_MARKER')
130
+ )
131
+ })
132
+ expect(sleepToolIndex).toBeGreaterThan(-1)
133
+
134
+ const userPapaIndex = after.findIndex((m) => {
135
+ return m.author.id === TEST_USER_ID && m.content.includes('papa')
136
+ })
137
+ expect(userPapaIndex).toBeGreaterThan(-1)
138
+ expect(sleepToolIndex).toBeLessThan(userPapaIndex)
139
+ const lastBotIndex = after.findLastIndex((m) => {
140
+ return m.author.id === ctx.discord.botUserId
141
+ })
142
+ expect(userPapaIndex).toBeLessThan(lastBotIndex)
143
+ },
144
+ 12_000,
145
+ )
146
+
147
+ test(
148
+ 'explicit abort emits MessageAbortedError and does not emit footer',
149
+ async () => {
150
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
151
+ content: 'Reply with exactly: abort-no-footer-setup',
152
+ })
153
+
154
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
155
+ timeout: 4_000,
156
+ predicate: (t) => {
157
+ return t.name === 'Reply with exactly: abort-no-footer-setup'
158
+ },
159
+ })
160
+
161
+ const th = ctx.discord.thread(thread.id)
162
+ await th.waitForBotReply({ timeout: 4_000 })
163
+
164
+ await waitForBotMessageContaining({
165
+ discord: ctx.discord,
166
+ threadId: thread.id,
167
+ userId: TEST_USER_ID,
168
+ text: '⋅',
169
+ timeout: 4_000,
170
+ })
171
+
172
+ await th.user(TEST_USER_ID).sendMessage({
173
+ content: 'SLOW_ABORT_MARKER run long response',
174
+ })
175
+
176
+ const runtime = getRuntime(thread.id)
177
+ expect(runtime).toBeDefined()
178
+ if (!runtime) {
179
+ throw new Error('Expected runtime to exist for abort no-footer test')
180
+ }
181
+
182
+ const beforeAbortMessages = await th.getMessages()
183
+ const baselineCount = beforeAbortMessages.length
184
+
185
+ runtime.abortActiveRun('test-no-footer-on-abort')
186
+
187
+ for (let i = 0; i < 10; i++) {
188
+ await new Promise((resolve) => {
189
+ setTimeout(resolve, 20)
190
+ })
191
+ const msgs = await th.getMessages()
192
+ const newMsgs = msgs.slice(baselineCount)
193
+ const hasFooter = newMsgs.some((m) => {
194
+ return m.author.id === ctx.discord.botUserId
195
+ && m.content.startsWith('*')
196
+ && m.content.includes('⋅')
197
+ })
198
+ expect(hasFooter).toBe(false)
199
+ }
200
+
201
+ expect(await th.text()).toMatchInlineSnapshot(`
202
+ "--- from: user (queue-advanced-tester)
203
+ Reply with exactly: abort-no-footer-setup
204
+ --- from: assistant (TestBot)
205
+ ⬥ ok
206
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
207
+ --- from: user (queue-advanced-tester)
208
+ SLOW_ABORT_MARKER run long response"
209
+ `)
210
+ },
211
+ 10_000,
212
+ )
213
+
214
+ test.skip(
215
+ 'explicit abort stale-idle window: follow-up prompt still gets assistant text',
216
+ async () => {
217
+ const setupPrompt = 'Reply with exactly: race-setup-1'
218
+ const raceFinalPrompt = 'Reply with exactly: race-final-1'
219
+
220
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
221
+ content: setupPrompt,
222
+ })
223
+
224
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
225
+ timeout: 4_000,
226
+ predicate: (t) => {
227
+ return t.name === setupPrompt
228
+ },
229
+ })
230
+
231
+ const th = ctx.discord.thread(thread.id)
232
+ const setupReply = await th.waitForBotReply({ timeout: 4_000 })
233
+ expect(setupReply.content.trim().length).toBeGreaterThan(0)
234
+
235
+ await th.user(TEST_USER_ID).sendMessage({
236
+ content: 'SLOW_ABORT_MARKER run long response',
237
+ })
238
+
239
+ const runtime = getRuntime(thread.id)
240
+ expect(runtime).toBeDefined()
241
+ if (!runtime) {
242
+ throw new Error('Expected runtime to exist for race abort scenario')
243
+ }
244
+
245
+ runtime.abortActiveRun('test-race-abort')
246
+
247
+ await th.user(TEST_USER_ID).sendMessage({
248
+ content: raceFinalPrompt,
249
+ })
250
+
251
+ await waitForBotReplyAfterUserMessage({
252
+ discord: ctx.discord,
253
+ threadId: thread.id,
254
+ userId: TEST_USER_ID,
255
+ userMessageIncludes: raceFinalPrompt,
256
+ timeout: 4_000,
257
+ })
258
+ },
259
+ 8_000,
260
+ )
261
+
262
+ test(
263
+ 'model switch mid-session aborts and restarts from same session history',
264
+ async () => {
265
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
266
+ content: 'Reply with exactly: retry-setup',
267
+ })
268
+
269
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
270
+ timeout: 4_000,
271
+ predicate: (t) => {
272
+ return t.name === 'Reply with exactly: retry-setup'
273
+ },
274
+ })
275
+
276
+ const th = ctx.discord.thread(thread.id)
277
+ const firstReply = await th.waitForBotReply({ timeout: 4_000 })
278
+ expect(firstReply.content.trim().length).toBeGreaterThan(0)
279
+
280
+ await th.user(TEST_USER_ID).sendMessage({
281
+ content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
282
+ })
283
+
284
+ await waitForBotMessageContaining({
285
+ discord: ctx.discord,
286
+ threadId: thread.id,
287
+ userId: TEST_USER_ID,
288
+ text: 'starting sleep',
289
+ afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
290
+ timeout: 4_000,
291
+ })
292
+
293
+ const sessionId = getThreadState(thread.id)?.sessionId
294
+ expect(sessionId).toBeDefined()
295
+ if (!sessionId) {
296
+ throw new Error('Expected active session id for model switch test')
297
+ }
298
+
299
+ await setSessionModel({
300
+ sessionId,
301
+ modelId: 'deterministic-provider/deterministic-v3',
302
+ variant: null,
303
+ })
304
+
305
+ const runtime = getRuntime(thread.id)
306
+ expect(runtime).toBeDefined()
307
+ if (!runtime) {
308
+ throw new Error('Expected runtime to exist for model switch test')
309
+ }
310
+ const retried = await runtime.retryLastUserPrompt()
311
+ expect(retried).toBe(true)
312
+
313
+ await th.user(TEST_USER_ID).sendMessage({
314
+ content: 'Reply with exactly: model-switch-followup',
315
+ })
316
+
317
+ await waitForBotReplyAfterUserMessage({
318
+ discord: ctx.discord,
319
+ threadId: thread.id,
320
+ userId: TEST_USER_ID,
321
+ userMessageIncludes: 'model-switch-followup',
322
+ timeout: 4_000,
323
+ })
324
+
325
+ // Wait for potential footer to arrive (race between step-finish interrupt
326
+ // and model switch settling means footer may or may not appear).
327
+ await new Promise((resolve) => {
328
+ setTimeout(resolve, 200)
329
+ })
330
+
331
+ const text = await th.text()
332
+ // The follow-up reply ("ok") must be present with deterministic-v3
333
+ expect(text).toContain('Reply with exactly: model-switch-followup')
334
+ expect(text).toContain('⬥ ok')
335
+ // The old sleep text should be visible from the first turn
336
+ expect(text).toContain('starting sleep 100')
337
+ },
338
+ 10_000,
339
+ )
340
+
341
+ test(
342
+ 'abortActiveRun settles correctly during long-running request',
343
+ async () => {
344
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
345
+ content: 'Reply with exactly: force-abort-setup',
346
+ })
347
+
348
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
349
+ timeout: 4_000,
350
+ predicate: (t) => {
351
+ return t.name === 'Reply with exactly: force-abort-setup'
352
+ },
353
+ })
354
+
355
+ const th = ctx.discord.thread(thread.id)
356
+ const setupReply = await th.waitForBotReply({ timeout: 4_000 })
357
+ expect(setupReply.content.trim().length).toBeGreaterThan(0)
358
+
359
+ await th.user(TEST_USER_ID).sendMessage({
360
+ content: 'SLOW_ABORT_MARKER run long response',
361
+ })
362
+
363
+ const runtime = getRuntime(thread.id)
364
+ expect(runtime).toBeDefined()
365
+ if (!runtime) {
366
+ throw new Error('Expected runtime to exist for forced-abort test')
367
+ }
368
+
369
+ runtime.abortActiveRun('force-abort-test')
370
+
371
+ expect(await th.text()).toMatchInlineSnapshot(`
372
+ "--- from: user (queue-advanced-tester)
373
+ Reply with exactly: force-abort-setup
374
+ --- from: assistant (TestBot)
375
+ ⬥ ok
376
+ --- from: user (queue-advanced-tester)
377
+ SLOW_ABORT_MARKER run long response"
378
+ `)
379
+ },
380
+ 10_000,
381
+ )
382
+ })
@@ -0,0 +1,268 @@
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
+
4
+ import { describe, test, expect } from 'vitest'
5
+ import {
6
+ setupQueueAdvancedSuite,
7
+ TEST_USER_ID,
8
+ } from './queue-advanced-e2e-setup.js'
9
+ import {
10
+ waitForBotMessageContaining,
11
+ waitForFooterMessage,
12
+ } from './test-utils.js'
13
+ import { getThreadSession } from './database.js'
14
+ import {
15
+ pendingActionButtonContexts,
16
+ showActionButtons,
17
+ } from './commands/action-buttons.js'
18
+
19
+ const TEXT_CHANNEL_ID = '200000000000001006'
20
+
21
+ async function waitForPendingActionButtons({
22
+ threadId,
23
+ timeoutMs,
24
+ }: {
25
+ threadId: string
26
+ timeoutMs: number
27
+ }): Promise<{ contextHash: string; messageId: string }> {
28
+ const start = Date.now()
29
+ while (Date.now() - start < timeoutMs) {
30
+ const entry = [...pendingActionButtonContexts.entries()].find(([, context]) => {
31
+ return context.thread.id === threadId && Boolean(context.messageId)
32
+ })
33
+ if (entry) {
34
+ const [contextHash, context] = entry
35
+ if (context.messageId) {
36
+ return { contextHash, messageId: context.messageId }
37
+ }
38
+ }
39
+ await new Promise<void>((resolve) => {
40
+ setTimeout(resolve, 100)
41
+ })
42
+ }
43
+ throw new Error('Timed out waiting for pending action buttons context')
44
+ }
45
+
46
+ async function waitForNoPendingActionButtons({
47
+ threadId,
48
+ timeoutMs,
49
+ }: {
50
+ threadId: string
51
+ timeoutMs: number
52
+ }): Promise<void> {
53
+ const start = Date.now()
54
+ while (Date.now() - start < timeoutMs) {
55
+ const stillPending = [...pendingActionButtonContexts.values()].some((context) => {
56
+ return context.thread.id === threadId
57
+ })
58
+ if (!stillPending) {
59
+ return
60
+ }
61
+ await new Promise<void>((resolve) => {
62
+ setTimeout(resolve, 100)
63
+ })
64
+ }
65
+ throw new Error('Timed out waiting for action buttons cleanup')
66
+ }
67
+
68
+ describe('queue advanced: action buttons', () => {
69
+ const ctx = setupQueueAdvancedSuite({
70
+ channelId: TEXT_CHANNEL_ID,
71
+ channelName: 'qa-action-buttons-e2e',
72
+ dirName: 'qa-action-buttons-e2e',
73
+ username: 'queue-action-tester',
74
+ })
75
+
76
+ test(
77
+ 'button click should continue the session with a follow-up assistant reply',
78
+ async () => {
79
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
80
+ content: 'Reply with exactly: action-button-setup',
81
+ })
82
+
83
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
84
+ timeout: 4_000,
85
+ predicate: (t) => {
86
+ return t.name === 'Reply with exactly: action-button-setup'
87
+ },
88
+ })
89
+
90
+ const th = ctx.discord.thread(thread.id)
91
+
92
+ await waitForBotMessageContaining({
93
+ discord: ctx.discord,
94
+ threadId: thread.id,
95
+ userId: TEST_USER_ID,
96
+ text: 'ok',
97
+ timeout: 4_000,
98
+ })
99
+
100
+ await waitForFooterMessage({
101
+ discord: ctx.discord,
102
+ threadId: thread.id,
103
+ timeout: 4_000,
104
+ afterMessageIncludes: 'ok',
105
+ afterAuthorId: ctx.discord.botUserId,
106
+ })
107
+
108
+ const currentSessionId = await getThreadSession(thread.id)
109
+ if (!currentSessionId) {
110
+ throw new Error('Expected thread session id before showing action buttons')
111
+ }
112
+
113
+ const channel = await ctx.botClient.channels.fetch(thread.id)
114
+ if (!channel || !channel.isThread()) {
115
+ throw new Error('Expected Discord thread channel for action button test')
116
+ }
117
+
118
+ await showActionButtons({
119
+ thread: channel,
120
+ sessionId: currentSessionId,
121
+ directory: ctx.directories.projectDirectory,
122
+ buttons: [{ label: 'Continue action-buttons flow', color: 'green' }],
123
+ })
124
+
125
+ const action = await waitForPendingActionButtons({
126
+ threadId: thread.id,
127
+ timeoutMs: 12_000,
128
+ })
129
+
130
+ await waitForBotMessageContaining({
131
+ discord: ctx.discord,
132
+ threadId: thread.id,
133
+ userId: TEST_USER_ID,
134
+ text: 'Action Required',
135
+ timeout: 12_000,
136
+ })
137
+
138
+ const interaction = await th.user(TEST_USER_ID).clickButton({
139
+ messageId: action.messageId,
140
+ customId: `action_button:${action.contextHash}:0`,
141
+ })
142
+
143
+ await th.waitForInteractionAck({
144
+ interactionId: interaction.id,
145
+ timeout: 4_000,
146
+ })
147
+
148
+ await waitForBotMessageContaining({
149
+ discord: ctx.discord,
150
+ threadId: thread.id,
151
+ text: 'action-buttons-click-continued',
152
+ timeout: 12_000,
153
+ })
154
+
155
+ await waitForFooterMessage({
156
+ discord: ctx.discord,
157
+ threadId: thread.id,
158
+ timeout: 12_000,
159
+ afterMessageIncludes: 'action-buttons-click-continued',
160
+ afterAuthorId: ctx.discord.botUserId,
161
+ })
162
+
163
+ const timeline = await th.text({ showInteractions: true })
164
+ expect(timeline).toMatchInlineSnapshot(`
165
+ "--- from: user (queue-action-tester)
166
+ Reply with exactly: action-button-setup
167
+ --- from: assistant (TestBot)
168
+ ⬥ ok
169
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
170
+ **Action Required**
171
+ _Selected: Continue action-buttons flow_
172
+ [user clicks button]
173
+ ⬥ action-buttons-click-continued
174
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
175
+ `)
176
+ expect(timeline).toContain('action-buttons-click-continued')
177
+ },
178
+ 20_000,
179
+ )
180
+
181
+ test(
182
+ 'manual thread message dismisses pending action buttons',
183
+ async () => {
184
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
185
+ content: 'Reply with exactly: action-button-dismiss-setup',
186
+ })
187
+
188
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
189
+ timeout: 4_000,
190
+ predicate: (t) => {
191
+ return t.name === 'Reply with exactly: action-button-dismiss-setup'
192
+ },
193
+ })
194
+
195
+ const th = ctx.discord.thread(thread.id)
196
+
197
+ await waitForBotMessageContaining({
198
+ discord: ctx.discord,
199
+ threadId: thread.id,
200
+ userId: TEST_USER_ID,
201
+ text: 'ok',
202
+ timeout: 4_000,
203
+ })
204
+
205
+ await waitForFooterMessage({
206
+ discord: ctx.discord,
207
+ threadId: thread.id,
208
+ timeout: 4_000,
209
+ afterMessageIncludes: 'ok',
210
+ afterAuthorId: ctx.discord.botUserId,
211
+ })
212
+
213
+ const currentSessionId = await getThreadSession(thread.id)
214
+ if (!currentSessionId) {
215
+ throw new Error('Expected thread session id before showing action buttons')
216
+ }
217
+
218
+ const channel = await ctx.botClient.channels.fetch(thread.id)
219
+ if (!channel || !channel.isThread()) {
220
+ throw new Error('Expected Discord thread channel for action button test')
221
+ }
222
+
223
+ await showActionButtons({
224
+ thread: channel,
225
+ sessionId: currentSessionId,
226
+ directory: ctx.directories.projectDirectory,
227
+ buttons: [{ label: 'Dismiss me', color: 'white' }],
228
+ })
229
+
230
+ await waitForPendingActionButtons({
231
+ threadId: thread.id,
232
+ timeoutMs: 4_000,
233
+ })
234
+
235
+ await th.user(TEST_USER_ID).sendMessage({
236
+ content: 'Reply with exactly: post-dismiss-user-message',
237
+ })
238
+
239
+ await waitForBotMessageContaining({
240
+ discord: ctx.discord,
241
+ threadId: thread.id,
242
+ text: 'Buttons dismissed.',
243
+ timeout: 4_000,
244
+ })
245
+
246
+ await waitForNoPendingActionButtons({
247
+ threadId: thread.id,
248
+ timeoutMs: 4_000,
249
+ })
250
+
251
+ const timeline = await th.text({ showInteractions: true })
252
+ expect(timeline).toMatchInlineSnapshot(`
253
+ "--- from: user (queue-action-tester)
254
+ Reply with exactly: action-button-dismiss-setup
255
+ --- from: assistant (TestBot)
256
+ ⬥ ok
257
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
258
+ **Action Required**
259
+ _Buttons dismissed._
260
+ --- from: user (queue-action-tester)
261
+ Reply with exactly: post-dismiss-user-message"
262
+ `)
263
+ expect(timeline).toContain('_Buttons dismissed._')
264
+ expect(timeline).toContain('post-dismiss-user-message')
265
+ },
266
+ 20_000,
267
+ )
268
+ })