@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,261 @@
1
+ // E2e test for question tool: user text message during pending question should
2
+ // dismiss the question (abort), then enqueue as a normal user prompt.
3
+ // The user's message must appear as a real user message in the thread, not
4
+ // get consumed as a tool result answer (which lost voice/image content).
5
+ import { describe, test, expect, afterEach } from 'vitest';
6
+ import { setupQueueAdvancedSuite, TEST_USER_ID } from './queue-advanced-e2e-setup.js';
7
+ import { waitForBotMessageContaining, waitForFooterMessage } from './test-utils.js';
8
+ import { store } from './store.js';
9
+ import { getOpencodeClient } from './opencode.js';
10
+ import { getThreadSession } from './database.js';
11
+ const TEXT_CHANNEL_ID = '200000000000001007';
12
+ const VOICE_CHANNEL_ID = '200000000000001017';
13
+ function setDeterministicTranscription(config) {
14
+ store.setState({
15
+ test: { deterministicTranscription: config },
16
+ });
17
+ }
18
+ function getOpencodeClientForTest(projectDirectory) {
19
+ const client = getOpencodeClient(projectDirectory);
20
+ if (!client) {
21
+ throw new Error('OpenCode client not found for project directory');
22
+ }
23
+ return client;
24
+ }
25
+ function getTextFromParts(parts) {
26
+ return parts.flatMap((part) => {
27
+ if (part.type === 'text') {
28
+ return [part.text];
29
+ }
30
+ return [];
31
+ });
32
+ }
33
+ function normalizeSessionText(text) {
34
+ return text
35
+ .replace(/\[current git branch is [^\]]+\]/g, '')
36
+ .replace(/<discord-user[^>]*\/>/g, '<discord-user />')
37
+ .trim();
38
+ }
39
+ function getSessionRoleTextTimeline(messages) {
40
+ return messages.flatMap((message) => {
41
+ const text = normalizeSessionText(getTextFromParts(message.parts).join(''));
42
+ if (!text.trim()) {
43
+ return [];
44
+ }
45
+ return [{ role: message.info.role, text }];
46
+ });
47
+ }
48
+ function getSessionMessageSummary(messages) {
49
+ return messages.map((message) => {
50
+ return {
51
+ role: message.info.role,
52
+ parts: message.parts.map((part) => {
53
+ if (part.type === 'text') {
54
+ return {
55
+ type: part.type,
56
+ text: normalizeSessionText(part.text),
57
+ };
58
+ }
59
+ if (part.type === 'tool') {
60
+ return {
61
+ type: part.type,
62
+ tool: part.tool,
63
+ status: part.state.status,
64
+ title: part.state.status === 'completed' ? part.state.title : undefined,
65
+ output: part.state.status === 'completed' ? part.state.output : undefined,
66
+ };
67
+ }
68
+ return { type: part.type };
69
+ }),
70
+ };
71
+ });
72
+ }
73
+ async function waitForSessionMessages({ projectDirectory, sessionId, timeoutMs, predicate, }) {
74
+ const client = getOpencodeClientForTest(projectDirectory);
75
+ const start = Date.now();
76
+ while (Date.now() - start < timeoutMs) {
77
+ const response = await client.session.messages({
78
+ sessionID: sessionId,
79
+ directory: projectDirectory,
80
+ });
81
+ const messages = response.data ?? [];
82
+ if (predicate(messages)) {
83
+ return messages;
84
+ }
85
+ await new Promise((resolve) => {
86
+ setTimeout(resolve, 100);
87
+ });
88
+ }
89
+ const finalResponse = await client.session.messages({
90
+ sessionID: sessionId,
91
+ directory: projectDirectory,
92
+ });
93
+ return finalResponse.data ?? [];
94
+ }
95
+ describe('queue advanced: question tool answer', () => {
96
+ const ctx = setupQueueAdvancedSuite({
97
+ channelId: TEXT_CHANNEL_ID,
98
+ channelName: 'qa-question-e2e',
99
+ dirName: 'qa-question-e2e',
100
+ username: 'queue-question-tester',
101
+ });
102
+ afterEach(() => {
103
+ setDeterministicTranscription(null);
104
+ });
105
+ test('user text message dismisses pending question and enqueues as normal prompt', async () => {
106
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
107
+ content: 'QUESTION_TEXT_ANSWER_MARKER',
108
+ });
109
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
110
+ timeout: 8_000,
111
+ predicate: (t) => {
112
+ return t.name === 'QUESTION_TEXT_ANSWER_MARKER';
113
+ },
114
+ });
115
+ const th = ctx.discord.thread(thread.id);
116
+ // Wait for the question dropdown message to appear in Discord.
117
+ // This is the user-visible signal that the question tool fired and
118
+ // kimaki processed the event. Avoids polling internal Maps which
119
+ // have timing sensitivity on slower CI hardware.
120
+ await waitForBotMessageContaining({
121
+ discord: ctx.discord,
122
+ threadId: thread.id,
123
+ text: 'Which option do you prefer?',
124
+ timeout: 12_000,
125
+ });
126
+ // User sends a text message while question is pending.
127
+ // This should:
128
+ // 1. Dismiss the pending question (cleanup context)
129
+ // 2. Abort the blocked session so OpenCode unblocks
130
+ // 3. Enqueue the message as a normal user prompt (not consumed as answer)
131
+ await th.user(TEST_USER_ID).sendMessage({
132
+ content: 'my text answer',
133
+ });
134
+ // Give time for question cleanup to propagate
135
+ await new Promise((r) => {
136
+ setTimeout(r, 1_000);
137
+ });
138
+ const timeline = await th.text({ showInteractions: true });
139
+ // The user's text answer must appear in Discord
140
+ expect(timeline).toContain('my text answer');
141
+ // The original question must have appeared
142
+ expect(timeline).toContain('Which option do you prefer?');
143
+ // The user's marker message triggered the question
144
+ expect(timeline).toContain('QUESTION_TEXT_ANSWER_MARKER');
145
+ }, 20_000);
146
+ });
147
+ describe('queue advanced: voice message during pending question', () => {
148
+ const ctx = setupQueueAdvancedSuite({
149
+ channelId: VOICE_CHANNEL_ID,
150
+ channelName: 'qa-question-voice-e2e',
151
+ dirName: 'qa-question-voice-e2e',
152
+ username: 'queue-question-tester',
153
+ });
154
+ afterEach(() => {
155
+ setDeterministicTranscription(null);
156
+ });
157
+ test('voice message during pending question dismisses question and transcribes normally', async () => {
158
+ // This is the exact bug scenario: user sends a voice message while a
159
+ // question dropdown is pending. Voice messages have empty message.content
160
+ // (audio is in attachments, transcription happens later). The old code
161
+ // passed "" as the question answer and consumed the message — the voice
162
+ // content was completely lost.
163
+ await ctx.discord.channel(VOICE_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
164
+ content: 'QUESTION_TEXT_ANSWER_MARKER',
165
+ });
166
+ const thread = await ctx.discord.channel(VOICE_CHANNEL_ID).waitForThread({
167
+ timeout: 8_000,
168
+ predicate: (t) => {
169
+ return t.name === 'QUESTION_TEXT_ANSWER_MARKER';
170
+ },
171
+ });
172
+ const th = ctx.discord.thread(thread.id);
173
+ // Wait for the question dropdown message to appear in Discord
174
+ await waitForBotMessageContaining({
175
+ discord: ctx.discord,
176
+ threadId: thread.id,
177
+ text: 'Which option do you prefer?',
178
+ timeout: 12_000,
179
+ });
180
+ // Send a voice message while the question is pending.
181
+ // Reproduction: Discord voice messages can still carry non-empty
182
+ // message.content. The bug consumed that raw text before transcription,
183
+ // so the session never received the spoken content.
184
+ setDeterministicTranscription({
185
+ transcription: 'I want option Alpha please',
186
+ queueMessage: false,
187
+ });
188
+ await th.user(TEST_USER_ID).sendVoiceMessage({
189
+ content: 'VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL',
190
+ });
191
+ // Give time for question cleanup to propagate
192
+ await new Promise((r) => {
193
+ setTimeout(r, 1_000);
194
+ });
195
+ // Voice content should be transcribed and appear as the next user message,
196
+ // processed after the model responds to the empty question answer.
197
+ await waitForBotMessageContaining({
198
+ discord: ctx.discord,
199
+ threadId: thread.id,
200
+ text: 'I want option Alpha please',
201
+ timeout: 8_000,
202
+ });
203
+ await waitForFooterMessage({
204
+ discord: ctx.discord,
205
+ threadId: thread.id,
206
+ timeout: 8_000,
207
+ afterMessageIncludes: 'I want option Alpha please',
208
+ afterAuthorId: ctx.discord.botUserId,
209
+ });
210
+ const sessionId = await getThreadSession(thread.id);
211
+ expect(sessionId).toBeTruthy();
212
+ const sessionMessages = await waitForSessionMessages({
213
+ projectDirectory: ctx.directories.projectDirectory,
214
+ sessionId: sessionId,
215
+ timeoutMs: 8_000,
216
+ predicate: (messages) => {
217
+ const timeline = getSessionRoleTextTimeline(messages);
218
+ return timeline.some((entry) => {
219
+ return entry.text.includes('I want option Alpha please');
220
+ });
221
+ },
222
+ });
223
+ const sessionTimeline = getSessionRoleTextTimeline(sessionMessages);
224
+ const sessionSummary = getSessionMessageSummary(sessionMessages);
225
+ const latestUserText = sessionTimeline
226
+ .filter((entry) => {
227
+ return entry.role === 'user';
228
+ })
229
+ .at(-1)?.text;
230
+ const assistantTexts = sessionTimeline.flatMap((entry) => {
231
+ if (entry.role === 'assistant') {
232
+ return [entry.text];
233
+ }
234
+ return [];
235
+ });
236
+ expect(latestUserText).toContain('I want option Alpha please');
237
+ expect(latestUserText).not.toContain('VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL');
238
+ expect(assistantTexts).toContain('ok');
239
+ expect(sessionSummary.some((message) => {
240
+ return message.role === 'user'
241
+ && message.parts.some((part) => {
242
+ return part.type === 'text' && part.text.includes('I want option Alpha please');
243
+ });
244
+ })).toBe(true);
245
+ expect(sessionSummary.some((message) => {
246
+ return message.role === 'assistant'
247
+ && message.parts.some((part) => {
248
+ return part.type === 'text' && part.text === 'ok';
249
+ });
250
+ })).toBe(true);
251
+ const timeline = await th.text({ showInteractions: true });
252
+ expect(timeline).toContain('QUESTION_TEXT_ANSWER_MARKER');
253
+ expect(timeline).toContain('Which option do you prefer?');
254
+ expect(timeline).toContain('VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL');
255
+ expect(timeline).toContain('🎤 Transcribing voice message...');
256
+ expect(timeline).toContain('📝 **Transcribed message:** I want option Alpha please');
257
+ expect(timeline).toContain('⬥ ok');
258
+ // Voice content must be present as a real transcribed message, not lost
259
+ expect(timeline).toContain('I want option Alpha please');
260
+ }, 20_000);
261
+ });
@@ -0,0 +1,114 @@
1
+ // E2e test for typing indicator lifecycle during interruption flow.
2
+ // Split from queue-advanced-typing.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 { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
6
+ const TEXT_CHANNEL_ID = '200000000000001008';
7
+ const e2eTest = describe;
8
+ e2eTest('queue advanced: typing interrupt', () => {
9
+ const ctx = setupQueueAdvancedSuite({
10
+ channelId: TEXT_CHANNEL_ID,
11
+ channelName: 'qa-typing-interrupt-e2e',
12
+ dirName: 'qa-typing-interrupt-e2e',
13
+ username: 'queue-advanced-tester',
14
+ });
15
+ test('interruption flow emits footer for final assistant reply and then stops typing', async () => {
16
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
17
+ content: 'Reply with exactly: typing-stop-interrupt-setup',
18
+ });
19
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
20
+ timeout: 4_000,
21
+ predicate: (t) => {
22
+ return t.name === 'Reply with exactly: typing-stop-interrupt-setup';
23
+ },
24
+ });
25
+ const th = ctx.discord.thread(thread.id);
26
+ await waitForBotMessageContaining({
27
+ discord: ctx.discord,
28
+ threadId: thread.id,
29
+ userId: TEST_USER_ID,
30
+ text: '*project',
31
+ timeout: 4_000,
32
+ });
33
+ th.clearTypingEvents();
34
+ await th.user(TEST_USER_ID).sendMessage({
35
+ content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
36
+ });
37
+ await waitForBotMessageContaining({
38
+ discord: ctx.discord,
39
+ threadId: thread.id,
40
+ userId: TEST_USER_ID,
41
+ text: 'starting sleep 100',
42
+ afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
43
+ timeout: 4_000,
44
+ });
45
+ await th.user(TEST_USER_ID).sendMessage({
46
+ content: 'Reply with exactly: typing-stop-interrupt-final',
47
+ });
48
+ await waitForBotMessageContaining({
49
+ discord: ctx.discord,
50
+ threadId: thread.id,
51
+ userId: TEST_USER_ID,
52
+ text: 'ok',
53
+ afterUserMessageIncludes: 'typing-stop-interrupt-final',
54
+ timeout: 12_000,
55
+ });
56
+ const messages = await waitForFooterMessage({
57
+ discord: ctx.discord,
58
+ threadId: thread.id,
59
+ timeout: 12_000,
60
+ afterMessageIncludes: 'typing-stop-interrupt-final',
61
+ afterAuthorId: TEST_USER_ID,
62
+ });
63
+ const finalUserIndex = messages.findIndex((message) => {
64
+ return message.author.id === TEST_USER_ID
65
+ && message.content.includes('typing-stop-interrupt-final');
66
+ });
67
+ const finalReplyIndex = messages.findIndex((message, index) => {
68
+ if (index <= finalUserIndex) {
69
+ return false;
70
+ }
71
+ return message.author.id === ctx.discord.botUserId && message.content.includes('ok');
72
+ });
73
+ const finalFooterIndex = messages.findIndex((message, index) => {
74
+ if (index <= finalReplyIndex) {
75
+ return false;
76
+ }
77
+ return message.author.id === ctx.discord.botUserId
78
+ && message.content.startsWith('*')
79
+ && message.content.includes('⋅');
80
+ });
81
+ expect(await th.text()).toMatchInlineSnapshot(`
82
+ "--- from: user (queue-advanced-tester)
83
+ Reply with exactly: typing-stop-interrupt-setup
84
+ --- from: assistant (TestBot)
85
+ ⬥ ok
86
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
87
+ --- from: user (queue-advanced-tester)
88
+ PLUGIN_TIMEOUT_SLEEP_MARKER
89
+ --- from: assistant (TestBot)
90
+ ⬥ starting sleep 100
91
+ --- from: user (queue-advanced-tester)
92
+ Reply with exactly: typing-stop-interrupt-final
93
+ --- from: assistant (TestBot)
94
+ ⬥ ok
95
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
96
+ `);
97
+ const timeline = await th.text({ showTyping: true });
98
+ expect(finalUserIndex).toBeGreaterThanOrEqual(0);
99
+ expect(finalReplyIndex).toBeGreaterThan(finalUserIndex);
100
+ expect(finalFooterIndex).toBeGreaterThan(finalReplyIndex);
101
+ expect(messages[finalFooterIndex]).toBeDefined();
102
+ const finalPromptPosition = timeline.indexOf('Reply with exactly: typing-stop-interrupt-final');
103
+ const finalReplyPosition = timeline.indexOf('--- from: assistant (TestBot)\n⬥ ok', finalPromptPosition);
104
+ const lastFooterPosition = timeline.lastIndexOf('*project ⋅');
105
+ expect(finalPromptPosition).toBeGreaterThanOrEqual(0);
106
+ expect(finalReplyPosition).toBeGreaterThan(finalPromptPosition);
107
+ expect(lastFooterPosition).toBeGreaterThanOrEqual(0);
108
+ const typingDuringFinalRun = timeline
109
+ .slice(finalPromptPosition, finalReplyPosition)
110
+ .match(/\[bot typing\]/g) || [];
111
+ expect(typingDuringFinalRun.length).toBeGreaterThanOrEqual(2);
112
+ expect(timeline.slice(lastFooterPosition)).not.toContain('[bot typing]');
113
+ }, 12_000);
114
+ });
@@ -0,0 +1,153 @@
1
+ // E2e tests for typing indicator lifecycle in advanced queue 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 { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
6
+ const TEXT_CHANNEL_ID = '200000000000001002';
7
+ const e2eTest = describe;
8
+ e2eTest('queue advanced: typing lifecycle', () => {
9
+ const ctx = setupQueueAdvancedSuite({
10
+ channelId: TEXT_CHANNEL_ID,
11
+ channelName: 'qa-typing-e2e',
12
+ dirName: 'qa-typing-e2e',
13
+ username: 'queue-advanced-tester',
14
+ });
15
+ test('normal reply stops typing after footer', async () => {
16
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
17
+ content: 'Reply with exactly: typing-stop-normal',
18
+ });
19
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
20
+ timeout: 4_000,
21
+ predicate: (t) => {
22
+ return t.name === 'Reply with exactly: typing-stop-normal';
23
+ },
24
+ });
25
+ const th = ctx.discord.thread(thread.id);
26
+ await th.waitForTypingEvent({ timeout: 1_000 }).catch(() => {
27
+ return undefined;
28
+ });
29
+ await waitForBotMessageContaining({
30
+ discord: ctx.discord,
31
+ threadId: thread.id,
32
+ userId: TEST_USER_ID,
33
+ text: 'ok',
34
+ timeout: 4_000,
35
+ });
36
+ const messages = await waitForFooterMessage({
37
+ discord: ctx.discord,
38
+ threadId: thread.id,
39
+ timeout: 4_000,
40
+ afterMessageIncludes: 'ok',
41
+ afterAuthorId: ctx.discord.botUserId,
42
+ });
43
+ const replyIndex = messages.findIndex((message) => {
44
+ return message.author.id === ctx.discord.botUserId && message.content.includes('ok');
45
+ });
46
+ const footerIndex = messages.findIndex((message, index) => {
47
+ if (index <= replyIndex) {
48
+ return false;
49
+ }
50
+ return message.author.id === ctx.discord.botUserId
51
+ && message.content.startsWith('*')
52
+ && message.content.includes('⋅');
53
+ });
54
+ const timeline = await th.text({ showTyping: true });
55
+ expect(timeline).toContain('Reply with exactly: typing-stop-normal');
56
+ expect(timeline).toContain('⬥ ok');
57
+ expect(timeline).toContain('*project ⋅ main ⋅');
58
+ const typingCount = (timeline.match(/\[bot typing\]/g) || []).length;
59
+ expect(typingCount).toBeGreaterThanOrEqual(1);
60
+ expect(replyIndex).toBeGreaterThanOrEqual(0);
61
+ expect(footerIndex).toBeGreaterThan(replyIndex);
62
+ expect(messages[footerIndex]).toBeDefined();
63
+ const lastFooterPosition = timeline.lastIndexOf('*project ⋅');
64
+ expect(lastFooterPosition).toBeGreaterThanOrEqual(0);
65
+ expect(timeline.slice(lastFooterPosition)).not.toContain('[bot typing]');
66
+ }, 8_000);
67
+ test('thread follow-up reply re-pulses typing after a visible assistant message while session stays busy', async () => {
68
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
69
+ content: 'Reply with exactly: typing-thread-reply-setup',
70
+ });
71
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
72
+ timeout: 4_000,
73
+ predicate: (t) => {
74
+ return t.name === 'Reply with exactly: typing-thread-reply-setup';
75
+ },
76
+ });
77
+ const th = ctx.discord.thread(thread.id);
78
+ await waitForBotMessageContaining({
79
+ discord: ctx.discord,
80
+ threadId: thread.id,
81
+ userId: TEST_USER_ID,
82
+ text: 'ok',
83
+ timeout: 4_000,
84
+ });
85
+ await waitForFooterMessage({
86
+ discord: ctx.discord,
87
+ threadId: thread.id,
88
+ timeout: 4_000,
89
+ afterMessageIncludes: 'ok',
90
+ afterAuthorId: ctx.discord.botUserId,
91
+ });
92
+ th.clearTypingEvents();
93
+ await th.user(TEST_USER_ID).sendMessage({
94
+ content: 'TYPING_REPULSE_MARKER',
95
+ });
96
+ const messagesAfterFirstReply = await waitForBotMessageContaining({
97
+ discord: ctx.discord,
98
+ threadId: thread.id,
99
+ userId: TEST_USER_ID,
100
+ text: 'repulse-first',
101
+ afterUserMessageIncludes: 'TYPING_REPULSE_MARKER',
102
+ timeout: 4_000,
103
+ });
104
+ const markerUserIndex = messagesAfterFirstReply.findIndex((message) => {
105
+ return message.author.id === TEST_USER_ID
106
+ && message.content.includes('TYPING_REPULSE_MARKER');
107
+ });
108
+ const firstReply = messagesAfterFirstReply.find((message, index) => {
109
+ if (index <= markerUserIndex) {
110
+ return false;
111
+ }
112
+ return message.author.id === ctx.discord.botUserId
113
+ && message.content.includes('repulse-first');
114
+ });
115
+ if (!firstReply) {
116
+ throw new Error('Expected first bot reply after TYPING_REPULSE_MARKER');
117
+ }
118
+ const typingAfterVisibleReply = await th.waitForTypingEvent({
119
+ timeout: 700,
120
+ afterTimestamp: new Date(firstReply.timestamp).getTime(),
121
+ }).then(() => {
122
+ return true;
123
+ }, () => {
124
+ return false;
125
+ });
126
+ const messages = await waitForFooterMessage({
127
+ discord: ctx.discord,
128
+ threadId: thread.id,
129
+ timeout: 6_000,
130
+ afterMessageIncludes: 'TYPING_REPULSE_MARKER',
131
+ afterAuthorId: TEST_USER_ID,
132
+ });
133
+ const timeline = await th.text({ showTyping: true });
134
+ expect(timeline).toContain('TYPING_REPULSE_MARKER');
135
+ expect(timeline).toContain('⬥ repulse-first');
136
+ const typingCount = (timeline.match(/\[bot typing\]/g) || []).length;
137
+ expect(typingCount).toBeGreaterThanOrEqual(2);
138
+ const followupUserIndex = messages.findIndex((message) => {
139
+ return message.author.id === TEST_USER_ID
140
+ && message.content.includes('TYPING_REPULSE_MARKER');
141
+ });
142
+ const followupReplyIndex = messages.findIndex((message, index) => {
143
+ if (index <= followupUserIndex) {
144
+ return false;
145
+ }
146
+ return message.author.id === ctx.discord.botUserId
147
+ && message.content.includes('repulse-first');
148
+ });
149
+ expect(followupUserIndex).toBeGreaterThanOrEqual(0);
150
+ expect(followupReplyIndex).toBeGreaterThan(followupUserIndex);
151
+ expect(typingAfterVisibleReply).toBe(true);
152
+ }, 10_000);
153
+ });
@@ -0,0 +1,119 @@
1
+ // E2e test: queued messages must drain immediately when the session is idle,
2
+ // even if action buttons are still pending. The isSessionBusy check is
3
+ // sufficient — hasPendingInteractiveUi() should NOT block queue drain.
4
+ import { describe, test, expect } from 'vitest';
5
+ import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
6
+ import { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
7
+ import { getThreadSession } from './database.js';
8
+ import { pendingActionButtonContexts, showActionButtons, } from './commands/action-buttons.js';
9
+ const TEXT_CHANNEL_ID = '200000000000001020';
10
+ describe('queue drain with pending interactive UI', () => {
11
+ const ctx = setupQueueAdvancedSuite({
12
+ channelId: TEXT_CHANNEL_ID,
13
+ channelName: 'qa-drain-interactive-ui',
14
+ dirName: 'qa-drain-interactive-ui',
15
+ username: 'drain-ui-tester',
16
+ });
17
+ test('queued message drains immediately while action buttons are still pending', async () => {
18
+ // 1. Create a thread with a first completed reply
19
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
20
+ content: 'Reply with exactly: drain-button-setup',
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: drain-button-setup';
26
+ },
27
+ });
28
+ const th = ctx.discord.thread(thread.id);
29
+ await waitForBotMessageContaining({
30
+ discord: ctx.discord,
31
+ threadId: thread.id,
32
+ userId: TEST_USER_ID,
33
+ text: 'ok',
34
+ timeout: 4_000,
35
+ });
36
+ await waitForFooterMessage({
37
+ discord: ctx.discord,
38
+ threadId: thread.id,
39
+ timeout: 4_000,
40
+ afterMessageIncludes: 'ok',
41
+ afterAuthorId: ctx.discord.botUserId,
42
+ });
43
+ // 2. Show action buttons (session is idle, buttons are pending)
44
+ const currentSessionId = await getThreadSession(thread.id);
45
+ if (!currentSessionId) {
46
+ throw new Error('Expected thread session id');
47
+ }
48
+ const channel = await ctx.botClient.channels.fetch(thread.id);
49
+ if (!channel || !channel.isThread()) {
50
+ throw new Error('Expected Discord thread channel');
51
+ }
52
+ await showActionButtons({
53
+ thread: channel,
54
+ sessionId: currentSessionId,
55
+ directory: ctx.directories.projectDirectory,
56
+ buttons: [{ label: 'Pending button', color: 'white' }],
57
+ });
58
+ // Verify buttons are pending
59
+ const start = Date.now();
60
+ while (Date.now() - start < 4_000) {
61
+ const entry = [...pendingActionButtonContexts.entries()].find(([, context]) => {
62
+ return context.thread.id === thread.id && Boolean(context.messageId);
63
+ });
64
+ if (entry) {
65
+ break;
66
+ }
67
+ await new Promise((resolve) => {
68
+ setTimeout(resolve, 100);
69
+ });
70
+ }
71
+ expect([...pendingActionButtonContexts.values()].some((c) => {
72
+ return c.thread.id === thread.id;
73
+ })).toBe(true);
74
+ // 3. Queue a message via /queue while buttons are still pending.
75
+ // The queue should drain immediately because session is idle.
76
+ // Currently FAILS: hasPendingInteractiveUi() blocks tryDrainQueue().
77
+ const { id: queueInteractionId } = await th.user(TEST_USER_ID)
78
+ .runSlashCommand({
79
+ name: 'queue',
80
+ options: [{ name: 'message', type: 3, value: 'Reply with exactly: post-button-drain' }],
81
+ });
82
+ const queueAck = await th.waitForInteractionAck({
83
+ interactionId: queueInteractionId,
84
+ timeout: 4_000,
85
+ });
86
+ if (!queueAck.messageId) {
87
+ throw new Error('Expected /queue response message id');
88
+ }
89
+ // 4. Queued message should dispatch immediately (not stay "Queued").
90
+ // The dispatch indicator should appear quickly.
91
+ await waitForBotMessageContaining({
92
+ discord: ctx.discord,
93
+ threadId: thread.id,
94
+ text: '» **drain-ui-tester:** Reply with exactly: post-button-drain',
95
+ timeout: 4_000,
96
+ });
97
+ // 5. Wait for the footer after the drained message completes
98
+ await waitForFooterMessage({
99
+ discord: ctx.discord,
100
+ threadId: thread.id,
101
+ timeout: 4_000,
102
+ afterMessageIncludes: '» **drain-ui-tester:**',
103
+ afterAuthorId: ctx.discord.botUserId,
104
+ });
105
+ const timeline = await th.text({ showInteractions: true });
106
+ expect(timeline).toMatchInlineSnapshot(`
107
+ "--- from: user (drain-ui-tester)
108
+ Reply with exactly: drain-button-setup
109
+ --- from: assistant (TestBot)
110
+ ⬥ ok
111
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
112
+ **Action Required**
113
+ [user interaction]
114
+ » **drain-ui-tester:** Reply with exactly: post-button-drain
115
+ ⬥ ok
116
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
117
+ `);
118
+ }, 20_000);
119
+ });