@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,314 @@
1
+ // In-process HTTP server speaking the Hrana v2 protocol.
2
+ // Backed by the `libsql` npm package (better-sqlite3 API).
3
+ // Binds to the fixed lock port for single-instance enforcement.
4
+ //
5
+ // Protocol logic is implemented in the `libsqlproxy` package.
6
+ // This file handles: server lifecycle, single-instance enforcement,
7
+ // auth, and kimaki-specific endpoints (/kimaki/wake, /health).
8
+ //
9
+ // Hrana v2 protocol spec ("Hrana over HTTP"):
10
+ // https://github.com/tursodatabase/libsql/blob/main/docs/HTTP_V2_SPEC.md
11
+
12
+ import fs from 'node:fs'
13
+ import http from 'node:http'
14
+ import path from 'node:path'
15
+ import crypto from 'node:crypto'
16
+ import Database from 'libsql'
17
+ import * as errore from 'errore'
18
+ import {
19
+ createLibsqlHandler,
20
+ createLibsqlNodeHandler,
21
+ libsqlExecutor,
22
+ } from 'libsqlproxy'
23
+ import { createLogger, LogPrefix } from './logger.js'
24
+ import { ServerStartError, FetchError } from './errors.js'
25
+ import { getLockPort } from './config.js'
26
+ import { store } from './store.js'
27
+
28
+ const hranaLogger = createLogger(LogPrefix.DB)
29
+
30
+ let db: Database.Database | null = null
31
+ let server: http.Server | null = null
32
+ let hranaUrl: string | null = null
33
+ let discordGatewayReady = false
34
+ let readyWaiters: Array<() => void> = []
35
+
36
+ export function markDiscordGatewayReady(): void {
37
+ if (discordGatewayReady) {
38
+ return
39
+ }
40
+ discordGatewayReady = true
41
+ for (const resolve of readyWaiters) {
42
+ resolve()
43
+ }
44
+ readyWaiters = []
45
+ }
46
+
47
+ async function waitForDiscordGatewayReady({ timeoutMs }: { timeoutMs: number }): Promise<boolean> {
48
+ if (discordGatewayReady) {
49
+ return true
50
+ }
51
+ const readyPromise = new Promise<boolean>((resolve) => {
52
+ readyWaiters.push(() => {
53
+ resolve(true)
54
+ })
55
+ })
56
+ const timeoutPromise = new Promise<boolean>((resolve) => {
57
+ setTimeout(() => {
58
+ resolve(false)
59
+ }, timeoutMs)
60
+ })
61
+ return Promise.race([readyPromise, timeoutPromise])
62
+ }
63
+
64
+ function getRequestAuthToken(req: http.IncomingMessage): string | null {
65
+ const authorizationHeader = req.headers.authorization
66
+ if (typeof authorizationHeader === 'string' && authorizationHeader.startsWith('Bearer ')) {
67
+ return authorizationHeader.slice('Bearer '.length)
68
+ }
69
+
70
+ return null
71
+ }
72
+
73
+ // Timing-safe comparison to prevent timing attacks when the hrana server
74
+ // is internet-facing (bindAll=true / KIMAKI_INTERNET_REACHABLE_URL set).
75
+ function isAuthorizedRequest(req: http.IncomingMessage): boolean {
76
+ const expectedToken = store.getState().gatewayToken
77
+ if (!expectedToken) {
78
+ return false
79
+ }
80
+ const providedToken = getRequestAuthToken(req)
81
+ if (!providedToken) {
82
+ return false
83
+ }
84
+ const expectedBuf = Buffer.from(expectedToken, 'utf8')
85
+ const providedBuf = Buffer.from(providedToken, 'utf8')
86
+ if (expectedBuf.length !== providedBuf.length) {
87
+ return false
88
+ }
89
+ return crypto.timingSafeEqual(expectedBuf, providedBuf)
90
+ }
91
+
92
+ function ensureServiceAuthTokenInStore(): string {
93
+ const existingToken = store.getState().gatewayToken
94
+ if (existingToken) {
95
+ return existingToken
96
+ }
97
+ const generatedToken = `${crypto.randomUUID()}:${crypto.randomBytes(32).toString('hex')}`
98
+ store.setState({ gatewayToken: generatedToken })
99
+ return generatedToken
100
+ }
101
+
102
+ /**
103
+ * Get the Hrana HTTP URL for injecting into plugin child processes.
104
+ * Returns null if the server hasn't been started yet.
105
+ * Only used for KIMAKI_DB_URL env var in opencode.ts — the bot process
106
+ * itself always uses direct file: access via Prisma.
107
+ */
108
+ export function getHranaUrl(): string | null {
109
+ return hranaUrl
110
+ }
111
+
112
+ /**
113
+ * Start the in-process Hrana v2 server on the fixed lock port.
114
+ * Handles single-instance enforcement: if the port is occupied, kills the
115
+ * existing process first.
116
+ */
117
+ export async function startHranaServer({
118
+ dbPath,
119
+ bindAll = false,
120
+ }: {
121
+ dbPath: string
122
+ /** Bind to 0.0.0.0 instead of 127.0.0.1. Set when KIMAKI_INTERNET_REACHABLE_URL is defined. */
123
+ bindAll?: boolean
124
+ }) {
125
+ if (server && db && hranaUrl) return hranaUrl
126
+
127
+ const port = getLockPort()
128
+ const bindHost = bindAll ? '0.0.0.0' : '127.0.0.1'
129
+ const serviceAuthToken = ensureServiceAuthTokenInStore()
130
+ process.env.KIMAKI_DB_AUTH_TOKEN = serviceAuthToken
131
+
132
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true })
133
+ await evictExistingInstance({ port })
134
+
135
+ hranaLogger.log(
136
+ `Starting hrana server on ${bindHost}:${port} with db: ${dbPath}`,
137
+ )
138
+
139
+ const database = new Database(dbPath)
140
+ database.exec('PRAGMA journal_mode = WAL')
141
+ database.exec('PRAGMA busy_timeout = 5000')
142
+ db = database
143
+
144
+ // Create the Hrana handler using libsqlproxy
145
+ const hranaFetchHandler = createLibsqlHandler(libsqlExecutor(database))
146
+ const hranaNodeHandler = createLibsqlNodeHandler(hranaFetchHandler)
147
+
148
+ // Combined handler: kimaki-specific endpoints + hrana protocol
149
+ const handler: http.RequestListener = async (req, res) => {
150
+ const pathname = new URL(req.url || '/', 'http://localhost').pathname
151
+ if (pathname === '/kimaki/wake') {
152
+ if (req.method !== 'POST') {
153
+ res.writeHead(405, { 'content-type': 'application/json' })
154
+ res.end(JSON.stringify({ error: 'method_not_allowed' }))
155
+ return
156
+ }
157
+ if (!isAuthorizedRequest(req)) {
158
+ res.writeHead(401, { 'content-type': 'application/json' })
159
+ res.end(JSON.stringify({ error: 'unauthorized' }))
160
+ return
161
+ }
162
+ const isReady = await waitForDiscordGatewayReady({ timeoutMs: 30_000 })
163
+ if (!isReady) {
164
+ res.writeHead(504, { 'content-type': 'application/json' })
165
+ res.end(JSON.stringify({ ready: false, error: 'timeout_waiting_for_discord_ready' }))
166
+ return
167
+ }
168
+ res.writeHead(200, { 'content-type': 'application/json' })
169
+ res.end(JSON.stringify({ ready: true }))
170
+ return
171
+ }
172
+ // Health check — no auth required
173
+ if (pathname === '/health') {
174
+ res.writeHead(200, { 'content-type': 'application/json' })
175
+ res.end(JSON.stringify({ status: 'ok', pid: process.pid }))
176
+ return
177
+ }
178
+ // Hrana routes: /v2, /v2/pipeline — require auth
179
+ if (pathname === '/v2' || pathname === '/v2/pipeline') {
180
+ if (!isAuthorizedRequest(req)) {
181
+ res.writeHead(401, { 'content-type': 'application/json' })
182
+ res.end(JSON.stringify({ error: 'unauthorized' }))
183
+ return
184
+ }
185
+ hranaNodeHandler(req, res)
186
+ return
187
+ }
188
+ res.writeHead(404)
189
+ res.end()
190
+ }
191
+
192
+ const started = await new Promise<ServerStartError | true>((resolve) => {
193
+ const srv = http.createServer(handler)
194
+
195
+ srv.on('error', (err: NodeJS.ErrnoException) => {
196
+ resolve(
197
+ new ServerStartError({
198
+ port,
199
+ reason:
200
+ err.code === 'EADDRINUSE'
201
+ ? `Port ${port} still in use after eviction`
202
+ : err.message,
203
+ }),
204
+ )
205
+ })
206
+ srv.listen(port, bindHost, () => {
207
+ server = srv
208
+ resolve(true)
209
+ })
210
+ })
211
+ if (started instanceof Error) {
212
+ database.close()
213
+ db = null
214
+ return started
215
+ }
216
+
217
+ hranaUrl = `http://127.0.0.1:${port}`
218
+ hranaLogger.log(`Hrana server ready at ${hranaUrl}`)
219
+ return hranaUrl
220
+ }
221
+
222
+ /**
223
+ * Stop the Hrana server and close the database.
224
+ */
225
+ export async function stopHranaServer() {
226
+ if (server) {
227
+ hranaLogger.log('Stopping hrana server...')
228
+ await new Promise<void>((resolve) => {
229
+ server!.close(() => {
230
+ resolve()
231
+ })
232
+ })
233
+ server = null
234
+ }
235
+ if (db) {
236
+ db.close()
237
+ db = null
238
+ }
239
+ hranaUrl = null
240
+ discordGatewayReady = false
241
+ readyWaiters = []
242
+ hranaLogger.log('Hrana server stopped')
243
+ }
244
+
245
+ // ── Single-instance enforcement ──────────────────────────────────────
246
+
247
+ /**
248
+ * Evict a previous kimaki instance on the lock port.
249
+ * Fetches /health to get the running process PID, then kills it directly.
250
+ * No lsof/netstat/spawnSync needed — the PID comes from the health response.
251
+ */
252
+ export async function evictExistingInstance({ port }: { port: number }) {
253
+ const url = `http://127.0.0.1:${port}/health`
254
+
255
+ const probe = await fetch(url, { signal: AbortSignal.timeout(1000) }).catch(
256
+ (e) => new FetchError({ url, cause: e }),
257
+ )
258
+ if (probe instanceof Error) return
259
+
260
+ const body = await (probe.json() as Promise<{ pid?: number }>).catch(
261
+ (e) => new FetchError({ url, cause: e }),
262
+ )
263
+ if (body instanceof Error) return
264
+
265
+ const targetPid = body.pid
266
+ if (!targetPid || targetPid === process.pid) return
267
+
268
+ hranaLogger.log(
269
+ `Evicting existing kimaki process (PID: ${targetPid}) on port ${port}`,
270
+ )
271
+ const killResult = errore.try({
272
+ try: () => {
273
+ process.kill(targetPid, 'SIGTERM')
274
+ },
275
+ catch: (e) =>
276
+ new Error('Failed to send SIGTERM to existing kimaki process', {
277
+ cause: e,
278
+ }),
279
+ })
280
+ if (killResult instanceof Error) {
281
+ hranaLogger.log(`Failed to kill PID ${targetPid}: ${killResult.message}`)
282
+ return
283
+ }
284
+
285
+ await new Promise((resolve) => {
286
+ setTimeout(resolve, 1000)
287
+ })
288
+
289
+ // Verify it's gone — if still alive, escalate to SIGKILL
290
+ const secondProbe = await fetch(url, {
291
+ signal: AbortSignal.timeout(500),
292
+ }).catch((e) => new FetchError({ url, cause: e }))
293
+ if (secondProbe instanceof Error) return
294
+
295
+ hranaLogger.log(`PID ${targetPid} still alive after SIGTERM, sending SIGKILL`)
296
+ const forceKillResult = errore.try({
297
+ try: () => {
298
+ process.kill(targetPid, 'SIGKILL')
299
+ },
300
+ catch: (e) =>
301
+ new Error('Failed to send SIGKILL to existing kimaki process', {
302
+ cause: e,
303
+ }),
304
+ })
305
+ if (forceKillResult instanceof Error) {
306
+ hranaLogger.log(
307
+ `Failed to force-kill PID ${targetPid}: ${forceKillResult.message}`,
308
+ )
309
+ return
310
+ }
311
+ await new Promise((resolve) => {
312
+ setTimeout(resolve, 1000)
313
+ })
314
+ }
@@ -0,0 +1,87 @@
1
+ import { afterEach, describe, expect, test } from 'vitest'
2
+ import {
3
+ buildHtmlActionCustomId,
4
+ cancelHtmlActionsForOwner,
5
+ cancelHtmlActionsForThread,
6
+ pendingHtmlActions,
7
+ registerHtmlAction,
8
+ } from './html-actions.js'
9
+
10
+ const TEST_OWNER_A = 'worktrees:user-a:channel-a'
11
+ const TEST_OWNER_B = 'worktrees:user-b:channel-a'
12
+
13
+ afterEach(() => {
14
+ cancelHtmlActionsForOwner(TEST_OWNER_A)
15
+ cancelHtmlActionsForOwner(TEST_OWNER_B)
16
+ })
17
+
18
+ describe('html action registry', () => {
19
+ test('registers action ids with expected custom id prefix', () => {
20
+ const actionId = registerHtmlAction({
21
+ ownerKey: TEST_OWNER_A,
22
+ run: async () => {
23
+ return undefined
24
+ },
25
+ })
26
+
27
+ expect(buildHtmlActionCustomId(actionId)).toMatch(/^html_action:/)
28
+ expect(pendingHtmlActions.has(actionId)).toBe(true)
29
+ })
30
+
31
+ test('cancels actions by owner', () => {
32
+ registerHtmlAction({
33
+ ownerKey: TEST_OWNER_A,
34
+ run: async () => {
35
+ return undefined
36
+ },
37
+ })
38
+ registerHtmlAction({
39
+ ownerKey: TEST_OWNER_A,
40
+ run: async () => {
41
+ return undefined
42
+ },
43
+ })
44
+
45
+ expect(cancelHtmlActionsForOwner(TEST_OWNER_A)).toBe(2)
46
+ expect(pendingHtmlActions.size).toBe(0)
47
+ })
48
+
49
+ test('cancels only actions from the matching thread', () => {
50
+ const threadAActionId = registerHtmlAction({
51
+ ownerKey: TEST_OWNER_A,
52
+ threadId: 'thread-a',
53
+ run: async () => {
54
+ return undefined
55
+ },
56
+ })
57
+ const threadBActionId = registerHtmlAction({
58
+ ownerKey: TEST_OWNER_B,
59
+ threadId: 'thread-b',
60
+ run: async () => {
61
+ return undefined
62
+ },
63
+ })
64
+
65
+ expect(cancelHtmlActionsForThread('thread-a')).toBe(1)
66
+ expect(pendingHtmlActions.has(threadAActionId)).toBe(false)
67
+ expect(pendingHtmlActions.has(threadBActionId)).toBe(true)
68
+ })
69
+
70
+ test('expires actions after ttl', async () => {
71
+ const actionId = registerHtmlAction({
72
+ ownerKey: TEST_OWNER_A,
73
+ ttlMs: 10,
74
+ run: async () => {
75
+ return undefined
76
+ },
77
+ })
78
+
79
+ await new Promise<void>((resolve) => {
80
+ setTimeout(() => {
81
+ resolve()
82
+ }, 30)
83
+ })
84
+
85
+ expect(pendingHtmlActions.has(actionId)).toBe(false)
86
+ })
87
+ })
@@ -0,0 +1,174 @@
1
+ // HTML action registry for rendered Discord components.
2
+ // Stores short-lived button callbacks by generated id so HTML-backed UI can
3
+ // attach interactions without leaking closures across rerenders.
4
+
5
+ import crypto from 'node:crypto'
6
+ import {
7
+ ComponentType,
8
+ MessageFlags,
9
+ type ButtonInteraction,
10
+ } from 'discord.js'
11
+ import { createLogger } from './logger.js'
12
+ import { notifyError } from './sentry.js'
13
+
14
+ const logger = createLogger('HTML_ACT')
15
+ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000
16
+
17
+ type PendingHtmlAction = {
18
+ actionId: string
19
+ ownerKey: string
20
+ threadId?: string
21
+ resolved: boolean
22
+ timer: ReturnType<typeof setTimeout>
23
+ run: ({ interaction }: { interaction: ButtonInteraction }) => Promise<void>
24
+ }
25
+
26
+ export const pendingHtmlActions = new Map<string, PendingHtmlAction>()
27
+ const actionIdsByOwner = new Map<string, Set<string>>()
28
+
29
+ export function buildHtmlActionCustomId(actionId: string): string {
30
+ return `html_action:${actionId}`
31
+ }
32
+
33
+ export function registerHtmlAction({
34
+ ownerKey,
35
+ threadId,
36
+ run,
37
+ ttlMs = DEFAULT_TTL_MS,
38
+ }: {
39
+ ownerKey: string
40
+ threadId?: string
41
+ run: ({ interaction }: { interaction: ButtonInteraction }) => Promise<void>
42
+ ttlMs?: number
43
+ }): string {
44
+ const actionId = crypto.randomBytes(8).toString('hex')
45
+ const timer = setTimeout(() => {
46
+ resolveHtmlAction({ actionId })
47
+ }, ttlMs)
48
+
49
+ pendingHtmlActions.set(actionId, {
50
+ actionId,
51
+ ownerKey,
52
+ threadId,
53
+ resolved: false,
54
+ timer,
55
+ run,
56
+ })
57
+
58
+ const ownerActionIds = actionIdsByOwner.get(ownerKey) ?? new Set<string>()
59
+ ownerActionIds.add(actionId)
60
+ actionIdsByOwner.set(ownerKey, ownerActionIds)
61
+ return actionId
62
+ }
63
+
64
+ export function cancelHtmlActionsForOwner(ownerKey: string): number {
65
+ const actionIds = actionIdsByOwner.get(ownerKey)
66
+ if (!actionIds) {
67
+ return 0
68
+ }
69
+
70
+ let cancelled = 0
71
+ for (const actionId of actionIds) {
72
+ const resolved = resolveHtmlAction({ actionId })
73
+ if (!resolved) {
74
+ continue
75
+ }
76
+ cancelled++
77
+ }
78
+
79
+ return cancelled
80
+ }
81
+
82
+ export function cancelHtmlActionsForThread(threadId: string): number {
83
+ let cancelled = 0
84
+
85
+ for (const [actionId, action] of pendingHtmlActions) {
86
+ if (action.threadId !== threadId) {
87
+ continue
88
+ }
89
+
90
+ const resolved = resolveHtmlAction({ actionId })
91
+ if (!resolved) {
92
+ continue
93
+ }
94
+ cancelled++
95
+ }
96
+
97
+ return cancelled
98
+ }
99
+
100
+ export async function handleHtmlActionButton(
101
+ interaction: ButtonInteraction,
102
+ ): Promise<void> {
103
+ const customId = interaction.customId
104
+ if (!customId.startsWith('html_action:')) {
105
+ return
106
+ }
107
+
108
+ const actionId = customId.slice('html_action:'.length)
109
+ if (!actionId) {
110
+ await interaction.reply({
111
+ content: 'Invalid action button.',
112
+ flags: MessageFlags.Ephemeral,
113
+ })
114
+ return
115
+ }
116
+
117
+ const action = pendingHtmlActions.get(actionId)
118
+ if (!action || action.resolved) {
119
+ await interaction.reply({
120
+ content: 'This action is no longer available.',
121
+ flags: MessageFlags.Ephemeral,
122
+ })
123
+ return
124
+ }
125
+
126
+ await interaction.deferUpdate()
127
+ const resolvedAction = resolveHtmlAction({ actionId })
128
+ if (!resolvedAction) {
129
+ return
130
+ }
131
+
132
+ try {
133
+ await resolvedAction.run({ interaction })
134
+ } catch (error) {
135
+ logger.error('[HTML_ACTION] Failed to run action:', error)
136
+ void notifyError(error, 'HTML action button failed')
137
+ await interaction
138
+ .editReply({
139
+ components: [
140
+ {
141
+ type: ComponentType.TextDisplay,
142
+ content: `Action failed: ${error instanceof Error ? error.message : String(error)}`,
143
+ },
144
+ ],
145
+ flags: MessageFlags.IsComponentsV2,
146
+ })
147
+ .catch(() => {
148
+ return undefined
149
+ })
150
+ }
151
+ }
152
+
153
+ function resolveHtmlAction({
154
+ actionId,
155
+ }: {
156
+ actionId: string
157
+ }): PendingHtmlAction | undefined {
158
+ const action = pendingHtmlActions.get(actionId)
159
+ if (!action || action.resolved) {
160
+ return undefined
161
+ }
162
+
163
+ action.resolved = true
164
+ clearTimeout(action.timer)
165
+ pendingHtmlActions.delete(actionId)
166
+
167
+ const ownerActionIds = actionIdsByOwner.get(action.ownerKey)
168
+ ownerActionIds?.delete(actionId)
169
+ if (ownerActionIds && ownerActionIds.size === 0) {
170
+ actionIdsByOwner.delete(action.ownerKey)
171
+ }
172
+
173
+ return action
174
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { parseInlineHtmlRenderables } from './html-components.js'
3
+
4
+ describe('parseInlineHtmlRenderables', () => {
5
+ test('parses text and button fragments', () => {
6
+ const result = parseInlineHtmlRenderables({
7
+ html: 'Before <button id="delete-a" variant="danger">Delete</button> after',
8
+ })
9
+ expect(result).toMatchInlineSnapshot(`
10
+ [
11
+ {
12
+ "text": "Before ",
13
+ "type": "text",
14
+ },
15
+ {
16
+ "disabled": false,
17
+ "id": "delete-a",
18
+ "label": "Delete",
19
+ "type": "button",
20
+ "variant": "danger",
21
+ },
22
+ {
23
+ "text": " after",
24
+ "type": "text",
25
+ },
26
+ ]
27
+ `)
28
+ })
29
+
30
+ test('rejects buttons without id', () => {
31
+ const result = parseInlineHtmlRenderables({
32
+ html: '<button>Delete</button>',
33
+ })
34
+ expect(result instanceof Error ? result.message : result).toBe(
35
+ '<button> is missing required id attribute',
36
+ )
37
+ })
38
+ })