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