@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,299 @@
1
+ // E2e test for /model switch behavior through interrupt recovery.
2
+ // Reproduces fallback where interrupt plugin resume can run without model,
3
+ // causing default opencode.json model to be used after switching session model.
4
+ import { describe, test, expect } from 'vitest';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
8
+ import { waitForBotMessageContaining, waitForBotReplyAfterUserMessage, waitForFooterMessage, waitForMessageById, } from './test-utils.js';
9
+ import { getThreadState } from './session-handler/thread-runtime-state.js';
10
+ import { getSessionModel } from './database.js';
11
+ import { initializeOpencodeForDirectory } from './opencode.js';
12
+ const TEXT_CHANNEL_ID = '200000000000001007';
13
+ function getCustomIdFromInteractionData({ serializedComponents, prefix, }) {
14
+ const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
15
+ const customIdRegex = new RegExp(`\"custom_id\"\\s*:\\s*\"(${escapedPrefix}[^\"]+)\"`);
16
+ const match = serializedComponents.match(customIdRegex);
17
+ if (!match?.[1]) {
18
+ throw new Error(`Could not find custom_id with prefix ${prefix} in components: ${serializedComponents}`);
19
+ }
20
+ return match[1];
21
+ }
22
+ async function waitForMessageComponentsWithCustomId({ discord, threadId, messageId, customIdPrefix, timeoutMs, }) {
23
+ const start = Date.now();
24
+ while (Date.now() - start < timeoutMs) {
25
+ const message = await waitForMessageById({
26
+ discord,
27
+ threadId,
28
+ messageId,
29
+ timeout: 1_000,
30
+ });
31
+ const serializedComponents = JSON.stringify(message.components);
32
+ if (serializedComponents.includes(customIdPrefix)) {
33
+ return serializedComponents;
34
+ }
35
+ await new Promise((resolve) => {
36
+ setTimeout(resolve, 50);
37
+ });
38
+ }
39
+ throw new Error(`Timed out waiting for custom_id prefix ${customIdPrefix} in message ${messageId}`);
40
+ }
41
+ async function waitForInteractionMessage({ getInteraction, interactionId, timeoutMs, }) {
42
+ const start = Date.now();
43
+ while (Date.now() - start < timeoutMs) {
44
+ const response = await getInteraction(interactionId);
45
+ if (response?.messageId) {
46
+ return {
47
+ messageId: response.messageId,
48
+ data: response.data || '',
49
+ };
50
+ }
51
+ await new Promise((resolve) => {
52
+ setTimeout(resolve, 50);
53
+ });
54
+ }
55
+ throw new Error(`Timed out waiting for interaction message ${interactionId}`);
56
+ }
57
+ describe('queue advanced: /model with interrupt recovery', () => {
58
+ const ctx = setupQueueAdvancedSuite({
59
+ channelId: TEXT_CHANNEL_ID,
60
+ channelName: 'qa-model-switch-e2e',
61
+ dirName: 'qa-model-switch-e2e',
62
+ username: 'queue-model-switch-tester',
63
+ });
64
+ test('session model selected in /model survives interrupt-plugin resume path', async () => {
65
+ const buildAgentDir = path.join(ctx.directories.projectDirectory, '.opencode', 'agent');
66
+ fs.mkdirSync(buildAgentDir, { recursive: true });
67
+ fs.writeFileSync(path.join(buildAgentDir, 'build.md'), [
68
+ '---',
69
+ 'name: build',
70
+ 'description: Default build agent for deterministic model tests',
71
+ 'model: deterministic-provider/deterministic-v2',
72
+ '---',
73
+ '',
74
+ 'You are the default build agent.',
75
+ '',
76
+ ].join('\n'));
77
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
78
+ content: 'Reply with exactly: model-switcher-setup',
79
+ });
80
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
81
+ timeout: 4_000,
82
+ predicate: (t) => {
83
+ return t.name === 'Reply with exactly: model-switcher-setup';
84
+ },
85
+ });
86
+ const th = ctx.discord.thread(thread.id);
87
+ await th.waitForBotReply({ timeout: 4_000 });
88
+ await waitForFooterMessage({
89
+ discord: ctx.discord,
90
+ threadId: thread.id,
91
+ timeout: 4_000,
92
+ });
93
+ const modelCommand = await th.user(TEST_USER_ID).runSlashCommand({
94
+ name: 'model',
95
+ });
96
+ await th.waitForInteractionAck({
97
+ interactionId: modelCommand.id,
98
+ timeout: 4_000,
99
+ });
100
+ const providerStep = await waitForInteractionMessage({
101
+ getInteraction: (interactionId) => {
102
+ return th.getInteractionResponse(interactionId);
103
+ },
104
+ interactionId: modelCommand.id,
105
+ timeoutMs: 4_000,
106
+ });
107
+ const providerCustomId = getCustomIdFromInteractionData({
108
+ serializedComponents: await waitForMessageComponentsWithCustomId({
109
+ discord: ctx.discord,
110
+ threadId: thread.id,
111
+ messageId: providerStep.messageId,
112
+ customIdPrefix: 'model_provider:',
113
+ timeoutMs: 4_000,
114
+ }),
115
+ prefix: 'model_provider:',
116
+ });
117
+ const providerSelect = await th.user(TEST_USER_ID).selectMenu({
118
+ messageId: providerStep.messageId,
119
+ customId: providerCustomId,
120
+ values: ['deterministic-provider'],
121
+ });
122
+ await th.waitForInteractionAck({
123
+ interactionId: providerSelect.id,
124
+ timeout: 4_000,
125
+ });
126
+ const modelStep = await waitForInteractionMessage({
127
+ getInteraction: (interactionId) => {
128
+ return th.getInteractionResponse(interactionId);
129
+ },
130
+ interactionId: providerSelect.id,
131
+ timeoutMs: 4_000,
132
+ });
133
+ const modelCustomId = getCustomIdFromInteractionData({
134
+ serializedComponents: await waitForMessageComponentsWithCustomId({
135
+ discord: ctx.discord,
136
+ threadId: thread.id,
137
+ messageId: modelStep.messageId,
138
+ customIdPrefix: 'model_select:',
139
+ timeoutMs: 4_000,
140
+ }),
141
+ prefix: 'model_select:',
142
+ });
143
+ const modelSelect = await th.user(TEST_USER_ID).selectMenu({
144
+ messageId: modelStep.messageId,
145
+ customId: modelCustomId,
146
+ values: ['deterministic-v3'],
147
+ });
148
+ await th.waitForInteractionAck({
149
+ interactionId: modelSelect.id,
150
+ timeout: 4_000,
151
+ });
152
+ const maybeVariantOrScopeStep = await waitForInteractionMessage({
153
+ getInteraction: (interactionId) => {
154
+ return th.getInteractionResponse(interactionId);
155
+ },
156
+ interactionId: modelSelect.id,
157
+ timeoutMs: 4_000,
158
+ });
159
+ const maybeVariantOrScopeMessage = await waitForMessageById({
160
+ discord: ctx.discord,
161
+ threadId: thread.id,
162
+ messageId: maybeVariantOrScopeStep.messageId,
163
+ timeout: 4_000,
164
+ });
165
+ const maybeVariantOrScopeComponents = JSON.stringify(maybeVariantOrScopeMessage.components);
166
+ const scopeStep = maybeVariantOrScopeComponents.includes('model_variant:')
167
+ ? await (async () => {
168
+ const variantCustomId = getCustomIdFromInteractionData({
169
+ serializedComponents: maybeVariantOrScopeComponents,
170
+ prefix: 'model_variant:',
171
+ });
172
+ const variantSelect = await th.user(TEST_USER_ID).selectMenu({
173
+ messageId: maybeVariantOrScopeStep.messageId,
174
+ customId: variantCustomId,
175
+ values: ['__none__'],
176
+ });
177
+ await th.waitForInteractionAck({
178
+ interactionId: variantSelect.id,
179
+ timeout: 4_000,
180
+ });
181
+ return waitForInteractionMessage({
182
+ getInteraction: (interactionId) => {
183
+ return th.getInteractionResponse(interactionId);
184
+ },
185
+ interactionId: variantSelect.id,
186
+ timeoutMs: 4_000,
187
+ });
188
+ })()
189
+ : maybeVariantOrScopeStep;
190
+ const scopeCustomId = getCustomIdFromInteractionData({
191
+ serializedComponents: await waitForMessageComponentsWithCustomId({
192
+ discord: ctx.discord,
193
+ threadId: thread.id,
194
+ messageId: scopeStep.messageId,
195
+ customIdPrefix: 'model_scope:',
196
+ timeoutMs: 4_000,
197
+ }),
198
+ prefix: 'model_scope:',
199
+ });
200
+ const scopeSelect = await th.user(TEST_USER_ID).selectMenu({
201
+ messageId: scopeStep.messageId,
202
+ customId: scopeCustomId,
203
+ values: ['session'],
204
+ });
205
+ await th.waitForInteractionAck({
206
+ interactionId: scopeSelect.id,
207
+ timeout: 4_000,
208
+ });
209
+ const sessionId = getThreadState(thread.id)?.sessionId;
210
+ expect(sessionId).toBeDefined();
211
+ if (!sessionId) {
212
+ throw new Error('Expected session id to be present after /model selection');
213
+ }
214
+ const sessionModel = await getSessionModel(sessionId);
215
+ expect(sessionModel?.modelId).toBe('deterministic-provider/deterministic-v3');
216
+ await th.user(TEST_USER_ID).sendMessage({
217
+ content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
218
+ });
219
+ await waitForBotMessageContaining({
220
+ discord: ctx.discord,
221
+ threadId: thread.id,
222
+ userId: TEST_USER_ID,
223
+ text: 'starting sleep',
224
+ afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
225
+ timeout: 4_000,
226
+ });
227
+ await th.user(TEST_USER_ID).sendMessage({
228
+ content: 'Reply with exactly: model-switcher-followup',
229
+ });
230
+ await waitForBotReplyAfterUserMessage({
231
+ discord: ctx.discord,
232
+ threadId: thread.id,
233
+ userId: TEST_USER_ID,
234
+ userMessageIncludes: 'model-switcher-followup',
235
+ timeout: 8_000,
236
+ });
237
+ const finalMessages = await waitForFooterMessage({
238
+ discord: ctx.discord,
239
+ threadId: thread.id,
240
+ timeout: 8_000,
241
+ afterMessageIncludes: 'model-switcher-followup',
242
+ afterAuthorId: TEST_USER_ID,
243
+ });
244
+ const footer = [...finalMessages].reverse().find((message) => {
245
+ return message.author.id === ctx.discord.botUserId
246
+ && message.content.startsWith('*')
247
+ && message.content.includes('⋅');
248
+ });
249
+ expect(await th.text()).toMatchInlineSnapshot(`
250
+ "--- from: user (queue-model-switch-tester)
251
+ Reply with exactly: model-switcher-setup
252
+ --- from: assistant (TestBot)
253
+ ⬥ ok
254
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
255
+ Model set for this session:
256
+ **Deterministic Provider** / **deterministic-v3**
257
+ \`deterministic-provider/deterministic-v3\`
258
+ _Restarting current request with new model..._
259
+ _Tip: create [agent .md files](https://github.com/remorses/kimaki/blob/main/docs/model-switching.md) in .opencode/agent/ for one-command model switching_
260
+ --- from: user (queue-model-switch-tester)
261
+ PLUGIN_TIMEOUT_SLEEP_MARKER
262
+ --- from: assistant (TestBot)
263
+ ⬥ ok
264
+ ⬥ starting sleep 100
265
+ --- from: user (queue-model-switch-tester)
266
+ Reply with exactly: model-switcher-followup
267
+ --- from: assistant (TestBot)
268
+ ⬥ ok
269
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v3*"
270
+ `);
271
+ expect(footer).toBeDefined();
272
+ expect(footer?.content).toContain('deterministic-v3');
273
+ const getClient = await initializeOpencodeForDirectory(ctx.directories.projectDirectory);
274
+ if (getClient instanceof Error) {
275
+ throw getClient;
276
+ }
277
+ const sessionMessagesResponse = await getClient().session.messages({
278
+ sessionID: sessionId,
279
+ directory: ctx.directories.projectDirectory,
280
+ });
281
+ const sessionMessages = sessionMessagesResponse.data || [];
282
+ const emptyUserMessagesWithDefaultModel = sessionMessages.filter((message) => {
283
+ if (message.info.role !== 'user') {
284
+ return false;
285
+ }
286
+ const hasNonEmptyTextPart = message.parts.some((part) => {
287
+ if (part.type !== 'text') {
288
+ return false;
289
+ }
290
+ return part.text.trim().length > 0;
291
+ });
292
+ if (hasNonEmptyTextPart) {
293
+ return false;
294
+ }
295
+ return message.info.model.modelID === 'deterministic-v2';
296
+ });
297
+ expect(emptyUserMessagesWithDefaultModel.length).toBe(0);
298
+ }, 20_000);
299
+ });
@@ -0,0 +1,180 @@
1
+ // E2e tests for typing indicator behavior around permission prompts.
2
+ import { describe, test, expect } from 'vitest';
3
+ import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
4
+ import { waitForBotMessageContaining, waitForBotReplyAfterUserMessage, waitForFooterMessage, } from './test-utils.js';
5
+ import { pendingPermissions } from './session-handler/thread-session-runtime.js';
6
+ const TEXT_CHANNEL_ID = '200000000000001005';
7
+ async function waitForPendingPermission({ threadId, timeoutMs, }) {
8
+ const start = Date.now();
9
+ while (Date.now() - start < timeoutMs) {
10
+ const threadPermissions = pendingPermissions.get(threadId);
11
+ const firstPermission = threadPermissions ? [...threadPermissions.values()][0] : undefined;
12
+ if (firstPermission?.contextHash && firstPermission.messageId) {
13
+ return {
14
+ contextHash: firstPermission.contextHash,
15
+ messageId: firstPermission.messageId,
16
+ };
17
+ }
18
+ await new Promise((resolve) => {
19
+ setTimeout(resolve, 100);
20
+ });
21
+ }
22
+ throw new Error('Timed out waiting for pending permission context');
23
+ }
24
+ describe('queue advanced: typing around permissions', () => {
25
+ const ctx = setupQueueAdvancedSuite({
26
+ channelId: TEXT_CHANNEL_ID,
27
+ channelName: 'qa-permission-typing-e2e',
28
+ dirName: 'qa-permission-typing-e2e',
29
+ username: 'queue-permission-tester',
30
+ });
31
+ test('permission prompt pauses typing until user click, then typing resumes for long follow-up step', async () => {
32
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
33
+ content: 'PERMISSION_TYPING_MARKER',
34
+ });
35
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
36
+ timeout: 4_000,
37
+ predicate: (t) => {
38
+ return t.name === 'PERMISSION_TYPING_MARKER';
39
+ },
40
+ });
41
+ const th = ctx.discord.thread(thread.id);
42
+ await th.waitForTypingEvent({ timeout: 4_000 });
43
+ const pending = await waitForPendingPermission({
44
+ threadId: thread.id,
45
+ timeoutMs: 4_000,
46
+ });
47
+ await waitForBotMessageContaining({
48
+ discord: ctx.discord,
49
+ threadId: thread.id,
50
+ userId: TEST_USER_ID,
51
+ text: 'Permission Required',
52
+ timeout: 4_000,
53
+ });
54
+ th.clearTypingEvents();
55
+ await th.waitForTypingEvent({ timeout: 2_000 }).then(() => {
56
+ throw new Error('Typing should stay paused while permission UI is pending');
57
+ }, () => {
58
+ return undefined;
59
+ });
60
+ const interaction = await th.user(TEST_USER_ID).clickButton({
61
+ messageId: pending.messageId,
62
+ customId: `permission_once:${pending.contextHash}`,
63
+ });
64
+ await th.waitForInteractionAck({
65
+ interactionId: interaction.id,
66
+ timeout: 4_000,
67
+ });
68
+ const resumedTyping = await th.waitForTypingEvent({ timeout: 9_000 });
69
+ expect(resumedTyping).toBeDefined();
70
+ await waitForBotMessageContaining({
71
+ discord: ctx.discord,
72
+ threadId: thread.id,
73
+ userId: TEST_USER_ID,
74
+ text: 'permission-flow-done',
75
+ timeout: 12_000,
76
+ });
77
+ await waitForFooterMessage({
78
+ discord: ctx.discord,
79
+ threadId: thread.id,
80
+ timeout: 12_000,
81
+ afterMessageIncludes: 'permission-flow-done',
82
+ afterAuthorId: ctx.discord.botUserId,
83
+ });
84
+ expect(await th.text({ showInteractions: true })).toMatchInlineSnapshot(`
85
+ "--- from: user (queue-permission-tester)
86
+ PERMISSION_TYPING_MARKER
87
+ --- from: assistant (TestBot)
88
+ ⬥ requesting external read permission
89
+ ⚠️ **Permission Required**
90
+ **Type:** \`external_directory\`
91
+ Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)
92
+ **Pattern:** \`/Users/morse/*\`
93
+ ✅ Permission **accepted**
94
+ [user clicks button]
95
+ ⬥ permission-flow-done
96
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
97
+ `);
98
+ const timeline = await th.text({
99
+ showTyping: true,
100
+ showInteractions: true,
101
+ });
102
+ const clickPosition = timeline.indexOf('[user clicks button]');
103
+ const donePosition = timeline.indexOf('⬥ permission-flow-done');
104
+ const footerPosition = timeline.lastIndexOf('*project ⋅');
105
+ expect(clickPosition).toBeGreaterThanOrEqual(0);
106
+ expect(donePosition).toBeGreaterThan(clickPosition);
107
+ expect(footerPosition).toBeGreaterThan(donePosition);
108
+ const afterClick = timeline.slice(clickPosition, donePosition);
109
+ const afterDone = timeline.slice(donePosition, footerPosition);
110
+ expect(afterClick).toContain('[bot typing]');
111
+ expect(afterDone).toContain('[bot typing]');
112
+ expect(timeline.slice(footerPosition)).not.toContain('[bot typing]');
113
+ }, 20_000);
114
+ test('manual thread message dismisses pending permission and sends the new prompt', async () => {
115
+ const initialPrompt = 'PERMISSION_TYPING_MARKER dismiss-flow';
116
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
117
+ content: initialPrompt,
118
+ });
119
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
120
+ timeout: 4_000,
121
+ predicate: (t) => {
122
+ return t.name === initialPrompt;
123
+ },
124
+ });
125
+ const th = ctx.discord.thread(thread.id);
126
+ await waitForPendingPermission({
127
+ threadId: thread.id,
128
+ timeoutMs: 4_000,
129
+ });
130
+ await waitForBotMessageContaining({
131
+ discord: ctx.discord,
132
+ threadId: thread.id,
133
+ userId: TEST_USER_ID,
134
+ text: 'Permission Required',
135
+ timeout: 4_000,
136
+ });
137
+ await th.user(TEST_USER_ID).sendMessage({
138
+ content: 'Reply with exactly: post-permission-user-message',
139
+ });
140
+ await waitForBotMessageContaining({
141
+ discord: ctx.discord,
142
+ threadId: thread.id,
143
+ text: 'Permission dismissed - user sent a new message.',
144
+ timeout: 8_000,
145
+ });
146
+ await waitForBotReplyAfterUserMessage({
147
+ discord: ctx.discord,
148
+ threadId: thread.id,
149
+ userId: TEST_USER_ID,
150
+ userMessageIncludes: 'post-permission-user-message',
151
+ timeout: 8_000,
152
+ });
153
+ await waitForBotMessageContaining({
154
+ discord: ctx.discord,
155
+ threadId: thread.id,
156
+ userId: TEST_USER_ID,
157
+ text: 'ok',
158
+ afterUserMessageIncludes: 'post-permission-user-message',
159
+ timeout: 8_000,
160
+ });
161
+ await waitForFooterMessage({
162
+ discord: ctx.discord,
163
+ threadId: thread.id,
164
+ timeout: 8_000,
165
+ afterMessageIncludes: 'ok',
166
+ afterAuthorId: ctx.discord.botUserId,
167
+ });
168
+ const timeline = await th.text({ showInteractions: true });
169
+ const normalizedTimeline = timeline.replace('⬥ requesting external read permission\n', '');
170
+ expect(normalizedTimeline).toContain('PERMISSION_TYPING_MARKER dismiss-flow');
171
+ expect(normalizedTimeline).toContain('Permission dismissed - user sent a new message.');
172
+ expect(normalizedTimeline).toContain('Reply with exactly: post-permission-user-message');
173
+ const followupUserPosition = normalizedTimeline.indexOf('Reply with exactly: post-permission-user-message');
174
+ const followupReplyPosition = normalizedTimeline.indexOf('⬥ ok', followupUserPosition);
175
+ const followupFooterPosition = normalizedTimeline.indexOf('*project ⋅', followupReplyPosition);
176
+ expect(followupUserPosition).toBeGreaterThanOrEqual(0);
177
+ expect(followupReplyPosition).toBeGreaterThan(followupUserPosition);
178
+ expect(followupFooterPosition).toBeGreaterThan(followupReplyPosition);
179
+ }, 20_000);
180
+ });