@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,533 @@
1
+ // OpenCode message part formatting for Discord.
2
+ // Converts SDK message parts (text, tools, reasoning) to Discord-friendly format,
3
+ // handles file attachments, and provides tool summary generation.
4
+
5
+ import type { Part, FilePartInput } from '@opencode-ai/sdk/v2'
6
+ import type { Message, TextChannel } from 'discord.js'
7
+
8
+ // Extended FilePartInput with original Discord URL for reference in prompts
9
+ export type DiscordFileAttachment = FilePartInput & {
10
+ sourceUrl?: string
11
+ }
12
+ import * as errore from 'errore'
13
+ import { createLogger, LogPrefix } from './logger.js'
14
+ import { FetchError } from './errors.js'
15
+ import { processImage } from './image-utils.js'
16
+ import { parsePatchFileCounts } from './patch-text-parser.js'
17
+
18
+ // Generic message type compatible with both v1 and v2 SDK
19
+ type GenericSessionMessage = {
20
+ info: { role: string; id?: string }
21
+ parts: Part[]
22
+ }
23
+
24
+ const logger = createLogger(LogPrefix.FORMATTING)
25
+
26
+ /**
27
+ * Resolves Discord mentions in message content to human-readable names.
28
+ * Replaces <@userId> with @displayName, <@&roleId> with @roleName, <#channelId> with #channelName.
29
+ */
30
+ export function resolveMentions(message: Message): string {
31
+ let content = message.content || ''
32
+
33
+ // Replace user mentions <@userId> or <@!userId> with @displayName
34
+ for (const [userId, user] of message.mentions.users) {
35
+ const member = message.guild?.members.cache.get(userId)
36
+ const displayName = member?.displayName || user.displayName || user.username
37
+ content = content.replace(
38
+ new RegExp(`<@!?${userId}>`, 'g'),
39
+ `@${displayName}`,
40
+ )
41
+ }
42
+
43
+ // Replace role mentions <@&roleId> with @roleName
44
+ for (const [roleId, role] of message.mentions.roles) {
45
+ content = content.replace(new RegExp(`<@&${roleId}>`, 'g'), `@${role.name}`)
46
+ }
47
+
48
+ // Replace channel mentions <#channelId> with #channelName
49
+ for (const [channelId, channel] of message.mentions.channels) {
50
+ const name = 'name' in channel ? (channel as TextChannel).name : channelId
51
+ content = content.replace(new RegExp(`<#${channelId}>`, 'g'), `#${name}`)
52
+ }
53
+
54
+ return content
55
+ }
56
+
57
+ /**
58
+ * Escapes Discord inline markdown characters so dynamic content
59
+ * doesn't break formatting when wrapped in *, _, **, etc.
60
+ */
61
+ function escapeInlineMarkdown(text: string): string {
62
+ return text.replace(/([*_~|`\\])/g, '\\$1')
63
+ }
64
+
65
+ // parsePatchCounts → imported from patch-text-parser.ts as parsePatchFileCounts
66
+
67
+ /**
68
+ * Normalize whitespace: convert newlines to spaces and collapse consecutive spaces.
69
+ */
70
+ function normalizeWhitespace(text: string): string {
71
+ return text.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ')
72
+ }
73
+
74
+ // A chunk of formatted content with associated part IDs, ready to be
75
+ // batched into as few Discord messages as possible.
76
+ export type SessionChunk = {
77
+ partIds: string[]
78
+ content: string
79
+ }
80
+
81
+ /**
82
+ * Collect renderable assistant parts from session messages as SessionChunks.
83
+ * Each non-empty formatted part becomes one chunk. Caller can batch them
84
+ * with batchChunksForDiscord() before sending.
85
+ *
86
+ * - skipPartIds: parts already synced (external sync). Skipped parts are
87
+ * not included in the result.
88
+ * - limit: max parts to include (from the end). Older parts are counted
89
+ * in skippedCount.
90
+ */
91
+ export function collectSessionChunks({
92
+ messages,
93
+ skipPartIds,
94
+ limit,
95
+ }: {
96
+ messages: GenericSessionMessage[]
97
+ skipPartIds?: Set<string>
98
+ limit?: number
99
+ }): { chunks: SessionChunk[]; skippedCount: number } {
100
+ const allChunks: SessionChunk[] = []
101
+
102
+ for (const message of messages) {
103
+ if (message.info.role !== 'assistant') {
104
+ continue
105
+ }
106
+ for (const part of message.parts) {
107
+ if (skipPartIds?.has(part.id)) {
108
+ continue
109
+ }
110
+ const content = formatPart(part)
111
+ if (!content.trim()) {
112
+ continue
113
+ }
114
+ allChunks.push({ partIds: [part.id], content: content.trimEnd() })
115
+ }
116
+ }
117
+
118
+ if (limit !== undefined && allChunks.length > limit) {
119
+ return {
120
+ chunks: allChunks.slice(-limit),
121
+ skippedCount: allChunks.length - limit,
122
+ }
123
+ }
124
+ return { chunks: allChunks, skippedCount: 0 }
125
+ }
126
+
127
+ // Merge consecutive SessionChunks into as few Discord messages as possible,
128
+ // respecting the 2000 char limit.
129
+ const DISCORD_BATCH_MAX_LENGTH = 2000
130
+
131
+ export function batchChunksForDiscord(chunks: SessionChunk[]): SessionChunk[] {
132
+ if (chunks.length === 0) {
133
+ return []
134
+ }
135
+ const batched: SessionChunk[] = []
136
+ let current: SessionChunk = { partIds: [...chunks[0]!.partIds], content: chunks[0]!.content }
137
+
138
+ for (let i = 1; i < chunks.length; i++) {
139
+ const next = chunks[i]!
140
+ const merged = current.content + '\n' + next.content
141
+ if (merged.length <= DISCORD_BATCH_MAX_LENGTH) {
142
+ current = {
143
+ partIds: [...current.partIds, ...next.partIds],
144
+ content: merged,
145
+ }
146
+ } else {
147
+ batched.push(current)
148
+ current = { partIds: [...next.partIds], content: next.content }
149
+ }
150
+ }
151
+ batched.push(current)
152
+ return batched
153
+ }
154
+
155
+ export const TEXT_MIME_TYPES = [
156
+ 'text/',
157
+ 'application/json',
158
+ 'application/xml',
159
+ 'application/javascript',
160
+ 'application/typescript',
161
+ 'application/x-yaml',
162
+ 'application/toml',
163
+ ]
164
+
165
+ export function isTextMimeType(contentType: string | null): boolean {
166
+ if (!contentType) {
167
+ return false
168
+ }
169
+ return TEXT_MIME_TYPES.some((prefix) => contentType.startsWith(prefix))
170
+ }
171
+
172
+ export async function getTextAttachments(message: Message): Promise<string> {
173
+ const textAttachments = Array.from(message.attachments.values()).filter(
174
+ (attachment) => isTextMimeType(attachment.contentType),
175
+ )
176
+
177
+ if (textAttachments.length === 0) {
178
+ return ''
179
+ }
180
+
181
+ const textContents = await Promise.all(
182
+ textAttachments.map(async (attachment) => {
183
+ const response = await errore.tryAsync({
184
+ try: () => fetch(attachment.url),
185
+ catch: (e) => new FetchError({ url: attachment.url, cause: e }),
186
+ })
187
+ if (response instanceof Error) {
188
+ return `<attachment filename="${attachment.name}" error="${response.message}" />`
189
+ }
190
+ if (!response.ok) {
191
+ return `<attachment filename="${attachment.name}" error="Failed to fetch: ${response.status}" />`
192
+ }
193
+ const text = await response.text()
194
+ return `<attachment filename="${attachment.name}" mime="${attachment.contentType}">\n${text}\n</attachment>`
195
+ }),
196
+ )
197
+
198
+ return textContents.join('\n\n')
199
+ }
200
+
201
+ export async function getFileAttachments(
202
+ message: Message,
203
+ ): Promise<DiscordFileAttachment[]> {
204
+ const fileAttachments = Array.from(message.attachments.values()).filter(
205
+ (attachment) => {
206
+ const contentType = attachment.contentType || ''
207
+ return (
208
+ contentType.startsWith('image/') || contentType === 'application/pdf'
209
+ )
210
+ },
211
+ )
212
+
213
+ if (fileAttachments.length === 0) {
214
+ return []
215
+ }
216
+
217
+ const results = await Promise.all(
218
+ fileAttachments.map(async (attachment) => {
219
+ const response = await errore.tryAsync({
220
+ try: () => fetch(attachment.url),
221
+ catch: (e) => new FetchError({ url: attachment.url, cause: e }),
222
+ })
223
+ if (response instanceof Error) {
224
+ logger.error(
225
+ `Error downloading attachment ${attachment.name}:`,
226
+ response.message,
227
+ )
228
+ return null
229
+ }
230
+ if (!response.ok) {
231
+ logger.error(
232
+ `Failed to fetch attachment ${attachment.name}: ${response.status}`,
233
+ )
234
+ return null
235
+ }
236
+
237
+ const rawBuffer = Buffer.from(await response.arrayBuffer())
238
+ const originalMime = attachment.contentType || 'application/octet-stream'
239
+
240
+ // Process image (resize if needed, convert to JPEG)
241
+ const { buffer, mime } = await processImage(rawBuffer, originalMime)
242
+
243
+ const base64 = buffer.toString('base64')
244
+ const dataUrl = `data:${mime};base64,${base64}`
245
+
246
+ logger.log(
247
+ `Attachment ${attachment.name}: ${rawBuffer.length} → ${buffer.length} bytes, ${mime}`,
248
+ )
249
+
250
+ return {
251
+ type: 'file' as const,
252
+ mime,
253
+ filename: attachment.name,
254
+ url: dataUrl,
255
+ sourceUrl: attachment.url,
256
+ }
257
+ }),
258
+ )
259
+
260
+ return results.filter((r) => r !== null) as DiscordFileAttachment[]
261
+ }
262
+
263
+ const MAX_BASH_COMMAND_INLINE_LENGTH = 100
264
+
265
+ export function getToolSummaryText(part: Part): string {
266
+ if (part.type !== 'tool') return ''
267
+
268
+ if (part.tool === 'edit') {
269
+ const filePath = (part.state.input?.filePath as string) || ''
270
+ const newString = (part.state.input?.newString as string) || ''
271
+ const oldString = (part.state.input?.oldString as string) || ''
272
+ const added = newString.split('\n').length
273
+ const removed = oldString.split('\n').length
274
+ const fileName = filePath.split('/').pop() || ''
275
+ return fileName
276
+ ? `*${escapeInlineMarkdown(fileName)}* (+${added}-${removed})`
277
+ : `(+${added}-${removed})`
278
+ }
279
+
280
+ if (part.tool === 'apply_patch') {
281
+ // Only inputs are available when parts are sent during streaming (output/metadata not yet populated)
282
+ const patchText = (part.state.input?.patchText as string) || ''
283
+ if (!patchText) {
284
+ return ''
285
+ }
286
+ const patchCounts = parsePatchFileCounts(patchText)
287
+ return [...patchCounts.entries()]
288
+ .map(([filePath, { additions, deletions }]) => {
289
+ const fileName = filePath.split('/').pop() || ''
290
+ return fileName
291
+ ? `*${escapeInlineMarkdown(fileName)}* (+${additions}-${deletions})`
292
+ : `(+${additions}-${deletions})`
293
+ })
294
+ .join(', ')
295
+ }
296
+
297
+ if (part.tool === 'write') {
298
+ const filePath = (part.state.input?.filePath as string) || ''
299
+ const content = (part.state.input?.content as string) || ''
300
+ const lines = content.split('\n').length
301
+ const fileName = filePath.split('/').pop() || ''
302
+ return fileName
303
+ ? `*${escapeInlineMarkdown(fileName)}* (${lines} line${lines === 1 ? '' : 's'})`
304
+ : `(${lines} line${lines === 1 ? '' : 's'})`
305
+ }
306
+
307
+ if (part.tool === 'webfetch') {
308
+ const url = (part.state.input?.url as string) || ''
309
+ const urlWithoutProtocol = url.replace(/^https?:\/\//, '')
310
+ return urlWithoutProtocol
311
+ ? `*${escapeInlineMarkdown(urlWithoutProtocol)}*`
312
+ : ''
313
+ }
314
+
315
+ if (part.tool === 'read') {
316
+ const filePath = (part.state.input?.filePath as string) || ''
317
+ const fileName = filePath.split('/').pop() || ''
318
+ return fileName ? `*${escapeInlineMarkdown(fileName)}*` : ''
319
+ }
320
+
321
+ if (part.tool === 'list') {
322
+ const path = (part.state.input?.path as string) || ''
323
+ const dirName = path.split('/').pop() || path
324
+ return dirName ? `*${escapeInlineMarkdown(dirName)}*` : ''
325
+ }
326
+
327
+ if (part.tool === 'glob') {
328
+ const pattern = (part.state.input?.pattern as string) || ''
329
+ return pattern ? `*${escapeInlineMarkdown(pattern)}*` : ''
330
+ }
331
+
332
+ if (part.tool === 'grep') {
333
+ const pattern = (part.state.input?.pattern as string) || ''
334
+ return pattern ? `*${escapeInlineMarkdown(pattern)}*` : ''
335
+ }
336
+
337
+ if (
338
+ part.tool === 'bash' ||
339
+ part.tool === 'todoread' ||
340
+ part.tool === 'todowrite'
341
+ ) {
342
+ return ''
343
+ }
344
+
345
+ // Task tool display is handled via subtask part in session-handler (shows name + agent)
346
+ if (part.tool === 'task') {
347
+ return ''
348
+ }
349
+
350
+ if (part.tool === 'skill') {
351
+ const name = (part.state.input?.name as string) || ''
352
+ return name ? `_${escapeInlineMarkdown(name)}_` : ''
353
+ }
354
+
355
+ // File upload tool - show the prompt
356
+ if (part.tool.endsWith('kimaki_file_upload')) {
357
+ const prompt = (part.state.input?.prompt as string) || ''
358
+ return prompt ? `*${escapeInlineMarkdown(prompt.slice(0, 60))}*` : ''
359
+ }
360
+
361
+ if (!part.state.input) return ''
362
+
363
+ const inputFields = Object.entries(part.state.input)
364
+ .map(([key, value]) => {
365
+ if (value === null || value === undefined) return null
366
+ const stringValue =
367
+ typeof value === 'string' ? value : JSON.stringify(value)
368
+ const normalized = normalizeWhitespace(stringValue)
369
+ const truncatedValue =
370
+ normalized.length > 50 ? normalized.slice(0, 50) + '…' : normalized
371
+ return `${key}: ${truncatedValue}`
372
+ })
373
+ .filter(Boolean)
374
+
375
+ if (inputFields.length === 0) return ''
376
+
377
+ return `(${inputFields.join(', ')})`
378
+ }
379
+
380
+ export function formatTodoList(part: Part): string {
381
+ if (part.type !== 'tool' || part.tool !== 'todowrite') return ''
382
+ const todos =
383
+ (part.state.input?.todos as {
384
+ content: string
385
+ status: 'pending' | 'in_progress' | 'completed' | 'cancelled'
386
+ }[]) || []
387
+ const activeIndex = todos.findIndex((todo) => {
388
+ return todo.status === 'in_progress'
389
+ })
390
+ const activeTodo = todos[activeIndex]
391
+ if (activeIndex === -1 || !activeTodo) return ''
392
+ // digit-with-period ⒈-⒛ for 1-20, fallback to regular number for 21+
393
+ const digitWithPeriod = '⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛'
394
+ const todoNumber = activeIndex + 1
395
+ const num =
396
+ todoNumber <= 20 ? digitWithPeriod[todoNumber - 1] : `${todoNumber}.`
397
+ const content =
398
+ activeTodo.content.charAt(0).toLowerCase() + activeTodo.content.slice(1)
399
+ return `${num} **${escapeInlineMarkdown(content)}**`
400
+ }
401
+
402
+ export function formatPart(part: Part, prefix?: string): string {
403
+ const pfx = prefix ? `${prefix} ⋅ ` : ''
404
+
405
+ if (part.type === 'text') {
406
+ const text = part.text?.trim()
407
+ if (!text) return ''
408
+ // For subtask text, always use bullet with prefix
409
+ if (prefix) {
410
+ return `⬥ ${pfx}${text}`
411
+ }
412
+ const firstChar = text[0] || ''
413
+ const markdownStarters = ['#', '*', '_', '-', '>', '`', '[', '|']
414
+ const startsWithMarkdown =
415
+ markdownStarters.includes(firstChar) || /^\d+\./.test(text)
416
+ if (startsWithMarkdown) {
417
+ return `\n${text}`
418
+ }
419
+ return `⬥ ${text}`
420
+ }
421
+
422
+ if (part.type === 'reasoning') {
423
+ if (!part.text?.trim()) return ''
424
+ return `┣ ${pfx}thinking`
425
+ }
426
+
427
+ if (part.type === 'file') {
428
+ return prefix
429
+ ? `📄 ${pfx}${part.filename || 'File'}`
430
+ : `📄 ${part.filename || 'File'}`
431
+ }
432
+
433
+ if (
434
+ part.type === 'step-start' ||
435
+ part.type === 'step-finish' ||
436
+ part.type === 'patch'
437
+ ) {
438
+ return ''
439
+ }
440
+
441
+ if (part.type === 'agent') {
442
+ return `┣ ${pfx}agent ${part.id}`
443
+ }
444
+
445
+ if (part.type === 'snapshot') {
446
+ return `┣ ${pfx}snapshot ${part.snapshot}`
447
+ }
448
+
449
+ if (part.type === 'tool') {
450
+ if (part.tool === 'todowrite') {
451
+ const formatted = formatTodoList(part)
452
+ return prefix && formatted ? `┣ ${pfx}${formatted}` : formatted
453
+ }
454
+
455
+ // Question tool is handled via Discord dropdowns, not text
456
+ if (part.tool === 'question') {
457
+ return ''
458
+ }
459
+
460
+ // File upload tool is handled via Discord button + modal, not text
461
+ if (part.tool.endsWith('kimaki_file_upload')) {
462
+ return ''
463
+ }
464
+
465
+ // Action buttons tool is handled via Discord buttons, not text
466
+ if (part.tool.endsWith('kimaki_action_buttons')) {
467
+ return ''
468
+ }
469
+
470
+ // Task tool display is handled in session-handler with proper label
471
+ if (part.tool === 'task') {
472
+ return ''
473
+ }
474
+
475
+ if (part.state.status === 'pending') {
476
+ if (part.tool !== 'bash') {
477
+ return ''
478
+ }
479
+ const command = (part.state.input?.command as string) || ''
480
+ const description = (part.state.input?.description as string) || ''
481
+ const isSingleLine = !command.includes('\n')
482
+ const toolTitle =
483
+ isSingleLine && command.length <= MAX_BASH_COMMAND_INLINE_LENGTH
484
+ ? ` _${escapeInlineMarkdown(command)}_`
485
+ : description
486
+ ? ` _${escapeInlineMarkdown(description)}_`
487
+ : ''
488
+ return `┣ ${pfx}bash${toolTitle}`
489
+ }
490
+
491
+ const summaryText = getToolSummaryText(part)
492
+ const stateTitle = 'title' in part.state ? part.state.title : undefined
493
+
494
+ let toolTitle = ''
495
+ if (part.state.status === 'error') {
496
+ toolTitle = part.state.error || 'error'
497
+ } else if (part.tool === 'bash') {
498
+ const command = (part.state.input?.command as string) || ''
499
+ const description = (part.state.input?.description as string) || ''
500
+ const isSingleLine = !command.includes('\n')
501
+ if (isSingleLine && command.length <= MAX_BASH_COMMAND_INLINE_LENGTH) {
502
+ toolTitle = `_${escapeInlineMarkdown(command)}_`
503
+ } else if (description) {
504
+ toolTitle = `_${escapeInlineMarkdown(description)}_`
505
+ } else if (stateTitle) {
506
+ toolTitle = `_${escapeInlineMarkdown(stateTitle)}_`
507
+ }
508
+ } else if (stateTitle) {
509
+ toolTitle = `_${escapeInlineMarkdown(stateTitle)}_`
510
+ }
511
+
512
+ const icon = (() => {
513
+ if (part.state.status === 'error') {
514
+ return '⨯'
515
+ }
516
+ if (
517
+ part.tool === 'edit' ||
518
+ part.tool === 'write' ||
519
+ part.tool === 'apply_patch'
520
+ ) {
521
+ return '◼︎'
522
+ }
523
+ return '┣'
524
+ })()
525
+ const toolParts = [part.tool, toolTitle, summaryText]
526
+ .filter(Boolean)
527
+ .join(' ')
528
+ return `${icon} ${pfx}${toolParts}`
529
+ }
530
+
531
+ logger.warn('Unknown part type:', part)
532
+ return ''
533
+ }