kimaki 0.4.82 → 0.4.84

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 (264) hide show
  1. package/LICENSE +21 -0
  2. package/dist/anthropic-auth-plugin.js +7 -0
  3. package/dist/cli.js +51 -7
  4. package/dist/commands/abort.js +5 -16
  5. package/dist/commands/action-buttons.js +3 -3
  6. package/dist/commands/add-project.js +1 -1
  7. package/dist/commands/ask-question.js +3 -3
  8. package/dist/commands/context-usage.js +1 -1
  9. package/dist/commands/create-new-project.js +1 -1
  10. package/dist/commands/fork.js +11 -8
  11. package/dist/commands/merge-worktree.js +1 -1
  12. package/dist/commands/new-worktree.js +63 -44
  13. package/dist/commands/remove-project.js +1 -1
  14. package/dist/commands/resume.js +11 -8
  15. package/dist/commands/screenshare.js +14 -6
  16. package/dist/commands/screenshare.test.js +20 -0
  17. package/dist/commands/session.js +1 -1
  18. package/dist/commands/undo-redo.js +91 -7
  19. package/dist/commands/user-command.js +1 -1
  20. package/dist/config.js +16 -1
  21. package/dist/database.js +53 -2
  22. package/dist/db.js +6 -0
  23. package/dist/discord-bot.js +48 -85
  24. package/dist/discord-command-registration.js +1 -1
  25. package/dist/external-opencode-sync.js +515 -0
  26. package/dist/external-opencode-sync.test.js +151 -0
  27. package/dist/gateway-proxy.e2e.test.js +8 -5
  28. package/dist/genai.js +1 -1
  29. package/dist/generated/enums.js +4 -0
  30. package/dist/generated/internal/class.js +4 -4
  31. package/dist/generated/internal/prismaNamespace.js +1 -0
  32. package/dist/generated/internal/prismaNamespaceBrowser.js +1 -0
  33. package/dist/generated/models/external_session_pending_prompts.js +1 -0
  34. package/dist/hrana-server.js +14 -285
  35. package/dist/hrana-server.test.js +4 -2
  36. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +7 -0
  37. package/dist/kimaki-opencode-plugin.js +2 -0
  38. package/dist/kitty-graphics-parser.js +3 -0
  39. package/dist/kitty-graphics-parser.test.js +276 -0
  40. package/dist/kitty-graphics-plugin.js +3 -0
  41. package/dist/markdown.js +4 -4
  42. package/dist/markdown.test.js +1 -1
  43. package/dist/message-formatting.js +54 -15
  44. package/dist/onboarding-tutorial.js +1 -1
  45. package/dist/openai-realtime.js +9 -13
  46. package/dist/opencode.js +28 -5
  47. package/dist/queue-advanced-e2e-setup.js +89 -0
  48. package/dist/queue-advanced-permissions-typing.e2e.test.js +5 -5
  49. package/dist/queue-advanced-typing.e2e.test.js +9 -22
  50. package/dist/queue-question-select-drain.e2e.test.js +117 -0
  51. package/dist/session-handler/event-stream-state.js +101 -7
  52. package/dist/session-handler/event-stream-state.test.js +7 -3
  53. package/dist/session-handler/thread-session-runtime.js +120 -9
  54. package/dist/store.js +1 -0
  55. package/dist/system-message.js +22 -4
  56. package/dist/system-message.test.js +19 -0
  57. package/dist/task-runner.js +1 -1
  58. package/dist/thread-message-queue.e2e.test.js +8 -14
  59. package/dist/tools.js +1 -1
  60. package/dist/undo-redo.e2e.test.js +20 -25
  61. package/package.json +10 -6
  62. package/schema.prisma +6 -0
  63. package/skills/errore/SKILL.md +40 -13
  64. package/skills/goke/SKILL.md +12 -0
  65. package/skills/lintcn/SKILL.md +868 -0
  66. package/skills/npm-package/SKILL.md +1 -0
  67. package/skills/proxyman/SKILL.md +215 -0
  68. package/skills/spiceflow/SKILL.md +1 -1
  69. package/skills/usecomputer/SKILL.md +339 -0
  70. package/src/ai-tool-to-genai.ts +1 -0
  71. package/src/anthropic-auth-plugin.ts +7 -0
  72. package/src/cli.ts +59 -6
  73. package/src/commands/abort.ts +6 -16
  74. package/src/commands/action-buttons.ts +5 -1
  75. package/src/commands/add-project.ts +1 -1
  76. package/src/commands/ask-question.ts +5 -2
  77. package/src/commands/context-usage.ts +1 -1
  78. package/src/commands/create-new-project.ts +1 -1
  79. package/src/commands/fork.ts +12 -11
  80. package/src/commands/merge-worktree.ts +1 -1
  81. package/src/commands/new-worktree.ts +74 -55
  82. package/src/commands/remove-project.ts +1 -1
  83. package/src/commands/resume.ts +12 -10
  84. package/src/commands/screenshare.test.ts +30 -0
  85. package/src/commands/screenshare.ts +18 -6
  86. package/src/commands/session.ts +1 -1
  87. package/src/commands/undo-redo.ts +108 -10
  88. package/src/commands/user-command.ts +1 -1
  89. package/src/config.ts +19 -1
  90. package/src/database.ts +72 -3
  91. package/src/db.ts +8 -0
  92. package/src/discord-bot.ts +58 -93
  93. package/src/discord-command-registration.ts +1 -1
  94. package/src/external-opencode-sync.ts +729 -0
  95. package/src/gateway-proxy.e2e.test.ts +9 -5
  96. package/src/genai.ts +3 -3
  97. package/src/generated/commonInputTypes.ts +34 -0
  98. package/src/generated/enums.ts +8 -0
  99. package/src/generated/internal/class.ts +4 -4
  100. package/src/generated/internal/prismaNamespace.ts +8 -0
  101. package/src/generated/internal/prismaNamespaceBrowser.ts +1 -0
  102. package/src/generated/models/thread_sessions.ts +53 -1
  103. package/src/hrana-server.test.ts +8 -2
  104. package/src/hrana-server.ts +18 -390
  105. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +7 -0
  106. package/src/kimaki-opencode-plugin.ts +2 -0
  107. package/src/markdown.test.ts +1 -1
  108. package/src/markdown.ts +4 -4
  109. package/src/message-formatting.ts +66 -17
  110. package/src/onboarding-tutorial.ts +1 -1
  111. package/src/openai-realtime.ts +6 -10
  112. package/src/opencode.ts +31 -7
  113. package/src/queue-advanced-e2e-setup.ts +92 -0
  114. package/src/queue-advanced-permissions-typing.e2e.test.ts +5 -5
  115. package/src/queue-advanced-typing.e2e.test.ts +9 -22
  116. package/src/queue-question-select-drain.e2e.test.ts +149 -0
  117. package/src/schema.sql +1 -0
  118. package/src/session-handler/event-stream-state.test.ts +7 -2
  119. package/src/session-handler/event-stream-state.ts +128 -7
  120. package/src/session-handler/thread-runtime-state.ts +5 -0
  121. package/src/session-handler/thread-session-runtime.ts +153 -11
  122. package/src/store.ts +8 -0
  123. package/src/system-message.ts +27 -4
  124. package/src/task-runner.ts +1 -1
  125. package/src/thread-message-queue.e2e.test.ts +8 -14
  126. package/src/tools.ts +1 -1
  127. package/src/undo-redo.e2e.test.ts +28 -26
  128. package/skills/jitter/node_modules/.bin/esbuild +0 -21
  129. package/skills/jitter/node_modules/.bin/tsc +0 -21
  130. package/skills/jitter/node_modules/.bin/tsserver +0 -21
  131. package/skills/jitter/node_modules/typescript/LICENSE.txt +0 -55
  132. package/skills/jitter/node_modules/typescript/README.md +0 -50
  133. package/skills/jitter/node_modules/typescript/SECURITY.md +0 -41
  134. package/skills/jitter/node_modules/typescript/ThirdPartyNoticeText.txt +0 -193
  135. package/skills/jitter/node_modules/typescript/bin/tsc +0 -2
  136. package/skills/jitter/node_modules/typescript/bin/tsserver +0 -2
  137. package/skills/jitter/node_modules/typescript/lib/_tsc.js +0 -133792
  138. package/skills/jitter/node_modules/typescript/lib/_tsserver.js +0 -659
  139. package/skills/jitter/node_modules/typescript/lib/_typingsInstaller.js +0 -222
  140. package/skills/jitter/node_modules/typescript/lib/cs/diagnosticMessages.generated.json +0 -2122
  141. package/skills/jitter/node_modules/typescript/lib/de/diagnosticMessages.generated.json +0 -2122
  142. package/skills/jitter/node_modules/typescript/lib/es/diagnosticMessages.generated.json +0 -2122
  143. package/skills/jitter/node_modules/typescript/lib/fr/diagnosticMessages.generated.json +0 -2122
  144. package/skills/jitter/node_modules/typescript/lib/it/diagnosticMessages.generated.json +0 -2122
  145. package/skills/jitter/node_modules/typescript/lib/ja/diagnosticMessages.generated.json +0 -2122
  146. package/skills/jitter/node_modules/typescript/lib/ko/diagnosticMessages.generated.json +0 -2122
  147. package/skills/jitter/node_modules/typescript/lib/lib.d.ts +0 -22
  148. package/skills/jitter/node_modules/typescript/lib/lib.decorators.d.ts +0 -384
  149. package/skills/jitter/node_modules/typescript/lib/lib.decorators.legacy.d.ts +0 -22
  150. package/skills/jitter/node_modules/typescript/lib/lib.dom.asynciterable.d.ts +0 -41
  151. package/skills/jitter/node_modules/typescript/lib/lib.dom.d.ts +0 -39429
  152. package/skills/jitter/node_modules/typescript/lib/lib.dom.iterable.d.ts +0 -571
  153. package/skills/jitter/node_modules/typescript/lib/lib.es2015.collection.d.ts +0 -147
  154. package/skills/jitter/node_modules/typescript/lib/lib.es2015.core.d.ts +0 -597
  155. package/skills/jitter/node_modules/typescript/lib/lib.es2015.d.ts +0 -28
  156. package/skills/jitter/node_modules/typescript/lib/lib.es2015.generator.d.ts +0 -77
  157. package/skills/jitter/node_modules/typescript/lib/lib.es2015.iterable.d.ts +0 -605
  158. package/skills/jitter/node_modules/typescript/lib/lib.es2015.promise.d.ts +0 -81
  159. package/skills/jitter/node_modules/typescript/lib/lib.es2015.proxy.d.ts +0 -128
  160. package/skills/jitter/node_modules/typescript/lib/lib.es2015.reflect.d.ts +0 -144
  161. package/skills/jitter/node_modules/typescript/lib/lib.es2015.symbol.d.ts +0 -46
  162. package/skills/jitter/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts +0 -326
  163. package/skills/jitter/node_modules/typescript/lib/lib.es2016.array.include.d.ts +0 -116
  164. package/skills/jitter/node_modules/typescript/lib/lib.es2016.d.ts +0 -21
  165. package/skills/jitter/node_modules/typescript/lib/lib.es2016.full.d.ts +0 -23
  166. package/skills/jitter/node_modules/typescript/lib/lib.es2016.intl.d.ts +0 -31
  167. package/skills/jitter/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts +0 -21
  168. package/skills/jitter/node_modules/typescript/lib/lib.es2017.d.ts +0 -26
  169. package/skills/jitter/node_modules/typescript/lib/lib.es2017.date.d.ts +0 -31
  170. package/skills/jitter/node_modules/typescript/lib/lib.es2017.full.d.ts +0 -23
  171. package/skills/jitter/node_modules/typescript/lib/lib.es2017.intl.d.ts +0 -44
  172. package/skills/jitter/node_modules/typescript/lib/lib.es2017.object.d.ts +0 -49
  173. package/skills/jitter/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts +0 -135
  174. package/skills/jitter/node_modules/typescript/lib/lib.es2017.string.d.ts +0 -45
  175. package/skills/jitter/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts +0 -53
  176. package/skills/jitter/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts +0 -77
  177. package/skills/jitter/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts +0 -53
  178. package/skills/jitter/node_modules/typescript/lib/lib.es2018.d.ts +0 -24
  179. package/skills/jitter/node_modules/typescript/lib/lib.es2018.full.d.ts +0 -24
  180. package/skills/jitter/node_modules/typescript/lib/lib.es2018.intl.d.ts +0 -83
  181. package/skills/jitter/node_modules/typescript/lib/lib.es2018.promise.d.ts +0 -30
  182. package/skills/jitter/node_modules/typescript/lib/lib.es2018.regexp.d.ts +0 -37
  183. package/skills/jitter/node_modules/typescript/lib/lib.es2019.array.d.ts +0 -79
  184. package/skills/jitter/node_modules/typescript/lib/lib.es2019.d.ts +0 -24
  185. package/skills/jitter/node_modules/typescript/lib/lib.es2019.full.d.ts +0 -24
  186. package/skills/jitter/node_modules/typescript/lib/lib.es2019.intl.d.ts +0 -23
  187. package/skills/jitter/node_modules/typescript/lib/lib.es2019.object.d.ts +0 -33
  188. package/skills/jitter/node_modules/typescript/lib/lib.es2019.string.d.ts +0 -37
  189. package/skills/jitter/node_modules/typescript/lib/lib.es2019.symbol.d.ts +0 -24
  190. package/skills/jitter/node_modules/typescript/lib/lib.es2020.bigint.d.ts +0 -765
  191. package/skills/jitter/node_modules/typescript/lib/lib.es2020.d.ts +0 -27
  192. package/skills/jitter/node_modules/typescript/lib/lib.es2020.date.d.ts +0 -42
  193. package/skills/jitter/node_modules/typescript/lib/lib.es2020.full.d.ts +0 -24
  194. package/skills/jitter/node_modules/typescript/lib/lib.es2020.intl.d.ts +0 -474
  195. package/skills/jitter/node_modules/typescript/lib/lib.es2020.number.d.ts +0 -28
  196. package/skills/jitter/node_modules/typescript/lib/lib.es2020.promise.d.ts +0 -47
  197. package/skills/jitter/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts +0 -99
  198. package/skills/jitter/node_modules/typescript/lib/lib.es2020.string.d.ts +0 -44
  199. package/skills/jitter/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts +0 -41
  200. package/skills/jitter/node_modules/typescript/lib/lib.es2021.d.ts +0 -23
  201. package/skills/jitter/node_modules/typescript/lib/lib.es2021.full.d.ts +0 -24
  202. package/skills/jitter/node_modules/typescript/lib/lib.es2021.intl.d.ts +0 -166
  203. package/skills/jitter/node_modules/typescript/lib/lib.es2021.promise.d.ts +0 -48
  204. package/skills/jitter/node_modules/typescript/lib/lib.es2021.string.d.ts +0 -33
  205. package/skills/jitter/node_modules/typescript/lib/lib.es2021.weakref.d.ts +0 -78
  206. package/skills/jitter/node_modules/typescript/lib/lib.es2022.array.d.ts +0 -121
  207. package/skills/jitter/node_modules/typescript/lib/lib.es2022.d.ts +0 -25
  208. package/skills/jitter/node_modules/typescript/lib/lib.es2022.error.d.ts +0 -75
  209. package/skills/jitter/node_modules/typescript/lib/lib.es2022.full.d.ts +0 -24
  210. package/skills/jitter/node_modules/typescript/lib/lib.es2022.intl.d.ts +0 -145
  211. package/skills/jitter/node_modules/typescript/lib/lib.es2022.object.d.ts +0 -26
  212. package/skills/jitter/node_modules/typescript/lib/lib.es2022.regexp.d.ts +0 -39
  213. package/skills/jitter/node_modules/typescript/lib/lib.es2022.string.d.ts +0 -25
  214. package/skills/jitter/node_modules/typescript/lib/lib.es2023.array.d.ts +0 -924
  215. package/skills/jitter/node_modules/typescript/lib/lib.es2023.collection.d.ts +0 -21
  216. package/skills/jitter/node_modules/typescript/lib/lib.es2023.d.ts +0 -22
  217. package/skills/jitter/node_modules/typescript/lib/lib.es2023.full.d.ts +0 -24
  218. package/skills/jitter/node_modules/typescript/lib/lib.es2023.intl.d.ts +0 -56
  219. package/skills/jitter/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts +0 -65
  220. package/skills/jitter/node_modules/typescript/lib/lib.es2024.collection.d.ts +0 -29
  221. package/skills/jitter/node_modules/typescript/lib/lib.es2024.d.ts +0 -26
  222. package/skills/jitter/node_modules/typescript/lib/lib.es2024.full.d.ts +0 -24
  223. package/skills/jitter/node_modules/typescript/lib/lib.es2024.object.d.ts +0 -29
  224. package/skills/jitter/node_modules/typescript/lib/lib.es2024.promise.d.ts +0 -35
  225. package/skills/jitter/node_modules/typescript/lib/lib.es2024.regexp.d.ts +0 -25
  226. package/skills/jitter/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts +0 -68
  227. package/skills/jitter/node_modules/typescript/lib/lib.es2024.string.d.ts +0 -29
  228. package/skills/jitter/node_modules/typescript/lib/lib.es5.d.ts +0 -4601
  229. package/skills/jitter/node_modules/typescript/lib/lib.es6.d.ts +0 -23
  230. package/skills/jitter/node_modules/typescript/lib/lib.esnext.array.d.ts +0 -35
  231. package/skills/jitter/node_modules/typescript/lib/lib.esnext.collection.d.ts +0 -96
  232. package/skills/jitter/node_modules/typescript/lib/lib.esnext.d.ts +0 -29
  233. package/skills/jitter/node_modules/typescript/lib/lib.esnext.decorators.d.ts +0 -28
  234. package/skills/jitter/node_modules/typescript/lib/lib.esnext.disposable.d.ts +0 -193
  235. package/skills/jitter/node_modules/typescript/lib/lib.esnext.error.d.ts +0 -24
  236. package/skills/jitter/node_modules/typescript/lib/lib.esnext.float16.d.ts +0 -443
  237. package/skills/jitter/node_modules/typescript/lib/lib.esnext.full.d.ts +0 -24
  238. package/skills/jitter/node_modules/typescript/lib/lib.esnext.intl.d.ts +0 -21
  239. package/skills/jitter/node_modules/typescript/lib/lib.esnext.iterator.d.ts +0 -148
  240. package/skills/jitter/node_modules/typescript/lib/lib.esnext.promise.d.ts +0 -34
  241. package/skills/jitter/node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts +0 -25
  242. package/skills/jitter/node_modules/typescript/lib/lib.scripthost.d.ts +0 -322
  243. package/skills/jitter/node_modules/typescript/lib/lib.webworker.asynciterable.d.ts +0 -41
  244. package/skills/jitter/node_modules/typescript/lib/lib.webworker.d.ts +0 -13150
  245. package/skills/jitter/node_modules/typescript/lib/lib.webworker.importscripts.d.ts +0 -23
  246. package/skills/jitter/node_modules/typescript/lib/lib.webworker.iterable.d.ts +0 -340
  247. package/skills/jitter/node_modules/typescript/lib/pl/diagnosticMessages.generated.json +0 -2122
  248. package/skills/jitter/node_modules/typescript/lib/pt-br/diagnosticMessages.generated.json +0 -2122
  249. package/skills/jitter/node_modules/typescript/lib/ru/diagnosticMessages.generated.json +0 -2122
  250. package/skills/jitter/node_modules/typescript/lib/tr/diagnosticMessages.generated.json +0 -2122
  251. package/skills/jitter/node_modules/typescript/lib/tsc.js +0 -8
  252. package/skills/jitter/node_modules/typescript/lib/tsserver.js +0 -8
  253. package/skills/jitter/node_modules/typescript/lib/tsserverlibrary.d.ts +0 -17
  254. package/skills/jitter/node_modules/typescript/lib/tsserverlibrary.js +0 -21
  255. package/skills/jitter/node_modules/typescript/lib/typesMap.json +0 -497
  256. package/skills/jitter/node_modules/typescript/lib/typescript.d.ts +0 -11438
  257. package/skills/jitter/node_modules/typescript/lib/typescript.js +0 -200253
  258. package/skills/jitter/node_modules/typescript/lib/typingsInstaller.js +0 -8
  259. package/skills/jitter/node_modules/typescript/lib/watchGuard.js +0 -53
  260. package/skills/jitter/node_modules/typescript/lib/zh-cn/diagnosticMessages.generated.json +0 -2122
  261. package/skills/jitter/node_modules/typescript/lib/zh-tw/diagnosticMessages.generated.json +0 -2122
  262. package/skills/jitter/node_modules/typescript/node_modules/.bin/tsc +0 -21
  263. package/skills/jitter/node_modules/typescript/node_modules/.bin/tsserver +0 -21
  264. package/skills/jitter/node_modules/typescript/package.json +0 -120
package/dist/markdown.js CHANGED
@@ -263,8 +263,8 @@ export function getCompactSessionContext({ client, sessionId, includeSystemPromp
263
263
  for (const msg of recentMessages) {
264
264
  if (msg.info.role === 'user') {
265
265
  const textParts = (msg.parts || [])
266
- .filter((p) => p.type === 'text' && 'text' in p)
267
- .map((p) => ('text' in p ? extractNonXmlContent(p.text || '') : ''))
266
+ .filter((p) => p.type === 'text')
267
+ .map((p) => (p.type === 'text' ? extractNonXmlContent(p.text || '') : ''))
268
268
  .filter(Boolean);
269
269
  if (textParts.length > 0) {
270
270
  lines.push(`[User]: ${textParts.join(' ').slice(0, 1000)}`);
@@ -274,8 +274,8 @@ export function getCompactSessionContext({ client, sessionId, includeSystemPromp
274
274
  else if (msg.info.role === 'assistant') {
275
275
  // Get assistant text parts (non-synthetic, non-empty)
276
276
  const textParts = (msg.parts || [])
277
- .filter((p) => p.type === 'text' && 'text' in p && !p.synthetic && p.text)
278
- .map((p) => ('text' in p ? p.text : ''))
277
+ .filter((p) => p.type === 'text' && !p.synthetic && p.text)
278
+ .map((p) => (p.type === 'text' ? p.text : ''))
279
279
  .filter(Boolean);
280
280
  if (textParts.length > 0) {
281
281
  lines.push(`[Assistant]: ${textParts.join(' ').slice(0, 1000)}`);
@@ -100,7 +100,7 @@ beforeAll(async () => {
100
100
  const msgs = await client.session.messages({ sessionID });
101
101
  const assistantMsg = msgs.data?.find((m) => m.info.role === 'assistant');
102
102
  const hasTextParts = assistantMsg?.parts?.some((p) => {
103
- return p.type === 'text' && 'text' in p && p.text && !('synthetic' in p && p.synthetic);
103
+ return p.type === 'text' && p.text && !p.synthetic;
104
104
  });
105
105
  if (hasTextParts) {
106
106
  // Extra wait for step-start and other parts to be flushed
@@ -45,26 +45,65 @@ function normalizeWhitespace(text) {
45
45
  return text.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ');
46
46
  }
47
47
  /**
48
- * Collects and formats the last N assistant parts from session messages.
49
- * Used by both /resume and /fork to show recent assistant context.
48
+ * Collect renderable assistant parts from session messages as SessionChunks.
49
+ * Each non-empty formatted part becomes one chunk. Caller can batch them
50
+ * with batchChunksForDiscord() before sending.
51
+ *
52
+ * - skipPartIds: parts already synced (external sync). Skipped parts are
53
+ * not included in the result.
54
+ * - limit: max parts to include (from the end). Older parts are counted
55
+ * in skippedCount.
50
56
  */
51
- export function collectLastAssistantParts({ messages, limit = 30, }) {
52
- const allAssistantParts = [];
57
+ export function collectSessionChunks({ messages, skipPartIds, limit, }) {
58
+ const allChunks = [];
53
59
  for (const message of messages) {
54
- if (message.info.role === 'assistant') {
55
- for (const part of message.parts) {
56
- const content = formatPart(part);
57
- if (content.trim()) {
58
- allAssistantParts.push({ id: part.id, content: content.trimEnd() });
59
- }
60
+ if (message.info.role !== 'assistant') {
61
+ continue;
62
+ }
63
+ for (const part of message.parts) {
64
+ if (skipPartIds?.has(part.id)) {
65
+ continue;
66
+ }
67
+ const content = formatPart(part);
68
+ if (!content.trim()) {
69
+ continue;
60
70
  }
71
+ allChunks.push({ partIds: [part.id], content: content.trimEnd() });
72
+ }
73
+ }
74
+ if (limit !== undefined && allChunks.length > limit) {
75
+ return {
76
+ chunks: allChunks.slice(-limit),
77
+ skippedCount: allChunks.length - limit,
78
+ };
79
+ }
80
+ return { chunks: allChunks, skippedCount: 0 };
81
+ }
82
+ // Merge consecutive SessionChunks into as few Discord messages as possible,
83
+ // respecting the 2000 char limit.
84
+ const DISCORD_BATCH_MAX_LENGTH = 2000;
85
+ export function batchChunksForDiscord(chunks) {
86
+ if (chunks.length === 0) {
87
+ return [];
88
+ }
89
+ const batched = [];
90
+ let current = { partIds: [...chunks[0].partIds], content: chunks[0].content };
91
+ for (let i = 1; i < chunks.length; i++) {
92
+ const next = chunks[i];
93
+ const merged = current.content + '\n' + next.content;
94
+ if (merged.length <= DISCORD_BATCH_MAX_LENGTH) {
95
+ current = {
96
+ partIds: [...current.partIds, ...next.partIds],
97
+ content: merged,
98
+ };
99
+ }
100
+ else {
101
+ batched.push(current);
102
+ current = { partIds: [...next.partIds], content: next.content };
61
103
  }
62
104
  }
63
- const partsToRender = allAssistantParts.slice(-limit);
64
- const partIds = partsToRender.map((p) => p.id);
65
- const content = partsToRender.map((p) => p.content).join('\n');
66
- const skippedCount = allAssistantParts.length - partsToRender.length;
67
- return { partIds, content, skippedCount };
105
+ batched.push(current);
106
+ return batched;
68
107
  }
69
108
  export const TEXT_MIME_TYPES = [
70
109
  'text/',
@@ -142,7 +142,7 @@ ${backticks}bash
142
142
  PORT=$((RANDOM % 6000 + 3000))
143
143
  tmux kill-session -t game-dev 2>/dev/null
144
144
  tmux new-session -d -s game-dev -c "$PWD"
145
- tmux send-keys -t game-dev "PORT=$PORT kimaki tunnel -p $PORT -- bun run server.ts" Enter
145
+ tmux send-keys -t game-dev "PORT=$PORT kimaki tunnel --kill -p $PORT -- bun run server.ts" Enter
146
146
  ${backticks}
147
147
 
148
148
  Wait a moment, then get the tunnel URL:
@@ -140,13 +140,11 @@ export async function startGenAiSession({ onAssistantAudioChunk, onAssistantStar
140
140
  }
141
141
  // Set up event handlers
142
142
  client.on('conversation.item.created', ({ item }) => {
143
- if ('role' in item &&
144
- item.role === 'assistant' &&
143
+ if (item.role === 'assistant' &&
145
144
  item.type === 'message') {
146
145
  // Check if this is the first audio content
147
- const hasAudio = 'content' in item &&
148
- Array.isArray(item.content) &&
149
- item.content.some((c) => 'type' in c && c.type === 'audio');
146
+ const hasAudio = Array.isArray(item.content) &&
147
+ item.content.some((c) => c.type === 'audio');
150
148
  if (hasAudio && !isAssistantSpeaking && onAssistantStartSpeaking) {
151
149
  isAssistantSpeaking = true;
152
150
  onAssistantStartSpeaking();
@@ -155,7 +153,7 @@ export async function startGenAiSession({ onAssistantAudioChunk, onAssistantStar
155
153
  });
156
154
  client.on('conversation.updated', ({ item, delta, }) => {
157
155
  // Handle audio chunks
158
- if (delta?.audio && 'role' in item && item.role === 'assistant') {
156
+ if (delta?.audio && item.role === 'assistant') {
159
157
  if (!isAssistantSpeaking && onAssistantStartSpeaking) {
160
158
  isAssistantSpeaking = true;
161
159
  onAssistantStartSpeaking();
@@ -177,13 +175,11 @@ export async function startGenAiSession({ onAssistantAudioChunk, onAssistantStar
177
175
  }
178
176
  // Handle transcriptions
179
177
  if (delta?.transcript) {
180
- if ('role' in item) {
181
- if (item.role === 'user') {
182
- openaiLogger.log('User transcription:', delta.transcript);
183
- }
184
- else if (item.role === 'assistant') {
185
- openaiLogger.log('Assistant transcription:', delta.transcript);
186
- }
178
+ if (item.role === 'user') {
179
+ openaiLogger.log('User transcription:', delta.transcript);
180
+ }
181
+ else if (item.role === 'assistant') {
182
+ openaiLogger.log('Assistant transcription:', delta.transcript);
187
183
  }
188
184
  }
189
185
  });
package/dist/opencode.js CHANGED
@@ -29,6 +29,9 @@ import { notifyError } from './sentry.js';
29
29
  import { DirectoryNotAccessibleError, ServerStartError, ServerNotReadyError, FetchError, } from './errors.js';
30
30
  import { ensureKimakiCommandShim, getPathEnvKey, getSpawnCommandAndArgs, prependPathEntry, selectResolvedCommand, } from './opencode-command.js';
31
31
  const opencodeLogger = createLogger(LogPrefix.OPENCODE);
32
+ // Tracks directories that have been initialized, to avoid repeated log spam
33
+ // from the external sync polling loop.
34
+ const initializedDirectories = new Set();
32
35
  const STARTUP_STDERR_TAIL_LIMIT = 30;
33
36
  const STARTUP_STDERR_LINE_MAX_LENGTH = 120;
34
37
  const STARTUP_ERROR_REASON_MAX_LENGTH = 1500;
@@ -361,6 +364,20 @@ async function startSingleServer() {
361
364
  opencodeLogger.warn(kimakiShimDirectory.message);
362
365
  }
363
366
  const gatewayToken = store.getState().gatewayToken;
367
+ const vitestOpencodeEnv = (() => {
368
+ if (process.env.KIMAKI_VITEST !== '1') {
369
+ return {};
370
+ }
371
+ const root = path.join(getDataDir(), 'opencode-vitest-home');
372
+ return {
373
+ OPENCODE_TEST_HOME: root,
374
+ OPENCODE_CONFIG_DIR: path.join(root, '.opencode-kimaki'),
375
+ XDG_CONFIG_HOME: path.join(root, '.config'),
376
+ XDG_DATA_HOME: path.join(root, '.local', 'share'),
377
+ XDG_CACHE_HOME: path.join(root, '.cache'),
378
+ XDG_STATE_HOME: path.join(root, '.local', 'state'),
379
+ };
380
+ })();
364
381
  const serverProcess = spawn(spawnCommand, spawnArgs, {
365
382
  stdio: 'pipe',
366
383
  detached: false,
@@ -417,6 +434,7 @@ async function startSingleServer() {
417
434
  ...(process.env.KIMAKI_SENTRY_DSN && {
418
435
  KIMAKI_SENTRY_DSN: process.env.KIMAKI_SENTRY_DSN,
419
436
  }),
437
+ ...vitestOpencodeEnv,
420
438
  ...(pathEnv && { [pathEnvKey]: pathEnv }),
421
439
  },
422
440
  });
@@ -475,10 +493,12 @@ async function startSingleServer() {
475
493
  singleServer = null;
476
494
  clientCache.clear();
477
495
  notifyServerLifecycle({ type: 'stopped' });
478
- // Intentional kills (SIGTERM from cleanup/restart) should not trigger
479
- // auto-restart. Only unexpected crashes (non-zero exit without signal)
480
- // get retried.
481
- if (signal === 'SIGTERM') {
496
+ // Intentional kills should not trigger auto-restart:
497
+ // - SIGTERM from our cleanup/restart code
498
+ // - SIGINT propagated from Ctrl+C (parent process group signal)
499
+ // - any exit during bot shutdown (shuttingDown flag)
500
+ // Only unexpected crashes (non-zero exit without signal) get retried.
501
+ if (signal === 'SIGTERM' || signal === 'SIGINT' || global.shuttingDown) {
482
502
  serverRetryCount = 0;
483
503
  return;
484
504
  }
@@ -586,7 +606,10 @@ export async function initializeOpencodeForDirectory(directory, _options) {
586
606
  if (server instanceof Error) {
587
607
  return server;
588
608
  }
589
- opencodeLogger.log(`Using shared server on port ${server.port} for directory: ${directory}`);
609
+ if (!initializedDirectories.has(directory)) {
610
+ initializedDirectories.add(directory);
611
+ opencodeLogger.log(`Using shared server on port ${server.port} for directory: ${directory}`);
612
+ }
590
613
  return () => {
591
614
  if (!singleServer) {
592
615
  throw new ServerNotReadyError({ directory });
@@ -299,6 +299,41 @@ export function createDeterministicMatchers() {
299
299
  ],
300
300
  },
301
301
  };
302
+ // Question tool for select+queue drain test: model asks a question via dropdown,
303
+ // user answers via select menu while a message is queued.
304
+ const questionSelectQueueMatcher = {
305
+ id: 'question-select-queue-marker',
306
+ priority: 107,
307
+ when: {
308
+ lastMessageRole: 'user',
309
+ latestUserTextIncludes: 'QUESTION_SELECT_QUEUE_MARKER',
310
+ },
311
+ then: {
312
+ parts: [
313
+ { type: 'stream-start', warnings: [] },
314
+ {
315
+ type: 'tool-call',
316
+ toolCallId: 'question-select-queue-call',
317
+ toolName: 'question',
318
+ input: JSON.stringify({
319
+ questions: [{
320
+ question: 'How to proceed?',
321
+ header: 'Select action',
322
+ options: [
323
+ { label: 'Alpha', description: 'Alpha option' },
324
+ { label: 'Beta', description: 'Beta option' },
325
+ ],
326
+ }],
327
+ }),
328
+ },
329
+ {
330
+ type: 'finish',
331
+ finishReason: 'tool-calls',
332
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
333
+ },
334
+ ],
335
+ },
336
+ };
302
337
  // Model responds with text + tool call, then after tool result the
303
338
  // follow-up matcher responds with text. This creates two assistant messages:
304
339
  // first with finish="tool-calls" + completed, second with finish="stop".
@@ -356,6 +391,57 @@ export function createDeterministicMatchers() {
356
391
  ],
357
392
  },
358
393
  };
394
+ const undoFileMatcher = {
395
+ id: 'undo-file-marker',
396
+ priority: 111,
397
+ when: {
398
+ lastMessageRole: 'user',
399
+ latestUserTextIncludes: 'UNDO_FILE_MARKER',
400
+ },
401
+ then: {
402
+ parts: [
403
+ { type: 'stream-start', warnings: [] },
404
+ { type: 'text-start', id: 'undo-file-text' },
405
+ { type: 'text-delta', id: 'undo-file-text', delta: 'creating undo file' },
406
+ { type: 'text-end', id: 'undo-file-text' },
407
+ {
408
+ type: 'tool-call',
409
+ toolCallId: 'undo-file-bash',
410
+ toolName: 'bash',
411
+ input: JSON.stringify({
412
+ command: 'mkdir -p tmp && printf created > tmp/undo-marker.txt',
413
+ description: 'Create undo marker file',
414
+ }),
415
+ },
416
+ {
417
+ type: 'finish',
418
+ finishReason: 'tool-calls',
419
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
420
+ },
421
+ ],
422
+ },
423
+ };
424
+ const undoFileFollowupMatcher = {
425
+ id: 'undo-file-followup',
426
+ priority: 112,
427
+ when: {
428
+ latestUserTextIncludes: 'UNDO_FILE_MARKER',
429
+ rawPromptIncludes: 'creating undo file',
430
+ },
431
+ then: {
432
+ parts: [
433
+ { type: 'stream-start', warnings: [] },
434
+ { type: 'text-start', id: 'undo-file-followup' },
435
+ { type: 'text-delta', id: 'undo-file-followup', delta: 'undo file created' },
436
+ { type: 'text-end', id: 'undo-file-followup' },
437
+ {
438
+ type: 'finish',
439
+ finishReason: 'stop',
440
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
441
+ },
442
+ ],
443
+ },
444
+ };
359
445
  // Multi-step tool chain: model emits text + 3 parallel tool calls in one
360
446
  // response (finish="tool-calls"). All tools complete, then the follow-up
361
447
  // matcher responds with final text (finish="stop"). This creates 2 assistant
@@ -562,10 +648,13 @@ export function createDeterministicMatchers() {
562
648
  pluginTimeoutSleepMatcher,
563
649
  actionButtonClickFollowupMatcher,
564
650
  questionToolMatcher,
651
+ questionSelectQueueMatcher,
565
652
  permissionTypingMatcher,
566
653
  permissionTypingFollowupMatcher,
567
654
  multiToolMatcher,
568
655
  multiToolFollowupMatcher,
656
+ undoFileMatcher,
657
+ undoFileFollowupMatcher,
569
658
  multiStepChainInitMatcher,
570
659
  multiStepChainStep2Matcher,
571
660
  multiStepChainStep3Matcher,
@@ -39,7 +39,7 @@ describe('queue advanced: typing around permissions', () => {
39
39
  },
40
40
  });
41
41
  const th = ctx.discord.thread(thread.id);
42
- await th.waitForTypingEvent({ timeout: 1_000 });
42
+ await th.waitForTypingEvent({ timeout: 4_000 });
43
43
  const pending = await waitForPendingPermission({
44
44
  threadId: thread.id,
45
45
  timeoutMs: 4_000,
@@ -133,14 +133,14 @@ describe('queue advanced: typing around permissions', () => {
133
133
  discord: ctx.discord,
134
134
  threadId: thread.id,
135
135
  text: 'Permission dismissed - user sent a new message.',
136
- timeout: 4_000,
136
+ timeout: 8_000,
137
137
  });
138
138
  await waitForBotReplyAfterUserMessage({
139
139
  discord: ctx.discord,
140
140
  threadId: thread.id,
141
141
  userId: TEST_USER_ID,
142
142
  userMessageIncludes: 'post-permission-user-message',
143
- timeout: 4_000,
143
+ timeout: 8_000,
144
144
  });
145
145
  await waitForBotMessageContaining({
146
146
  discord: ctx.discord,
@@ -148,12 +148,12 @@ describe('queue advanced: typing around permissions', () => {
148
148
  userId: TEST_USER_ID,
149
149
  text: 'ok',
150
150
  afterUserMessageIncludes: 'post-permission-user-message',
151
- timeout: 4_000,
151
+ timeout: 8_000,
152
152
  });
153
153
  await waitForFooterMessage({
154
154
  discord: ctx.discord,
155
155
  threadId: thread.id,
156
- timeout: 4_000,
156
+ timeout: 8_000,
157
157
  afterMessageIncludes: 'ok',
158
158
  afterAuthorId: ctx.discord.botUserId,
159
159
  });
@@ -52,14 +52,11 @@ e2eTest('queue advanced: typing lifecycle', () => {
52
52
  && message.content.includes('⋅');
53
53
  });
54
54
  const timeline = await th.text({ showTyping: true });
55
- expect(timeline).toMatchInlineSnapshot(`
56
- "--- from: user (queue-advanced-tester)
57
- Reply with exactly: typing-stop-normal
58
- [bot typing]
59
- --- from: assistant (TestBot)
60
- ⬥ ok
61
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
62
- `);
55
+ expect(timeline).toContain('Reply with exactly: typing-stop-normal');
56
+ expect(timeline).toContain('⬥ ok');
57
+ expect(timeline).toContain('*project main ⋅');
58
+ const typingCount = (timeline.match(/\[bot typing\]/g) || []).length;
59
+ expect(typingCount).toBeGreaterThanOrEqual(1);
63
60
  expect(replyIndex).toBeGreaterThanOrEqual(0);
64
61
  expect(footerIndex).toBeGreaterThan(replyIndex);
65
62
  expect(messages[footerIndex]).toBeDefined();
@@ -134,20 +131,10 @@ e2eTest('queue advanced: typing lifecycle', () => {
134
131
  afterAuthorId: TEST_USER_ID,
135
132
  });
136
133
  const timeline = await th.text({ showTyping: true });
137
- expect(timeline).toMatchInlineSnapshot(`
138
- "--- from: user (queue-advanced-tester)
139
- Reply with exactly: typing-thread-reply-setup
140
- --- from: assistant (TestBot)
141
- ⬥ ok
142
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
143
- --- from: user (queue-advanced-tester)
144
- TYPING_REPULSE_MARKER
145
- [bot typing]
146
- --- from: assistant (TestBot)
147
- ⬥ repulse-first
148
- [bot typing]
149
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
150
- `);
134
+ expect(timeline).toContain('TYPING_REPULSE_MARKER');
135
+ expect(timeline).toContain('⬥ repulse-first');
136
+ const typingCount = (timeline.match(/\[bot typing\]/g) || []).length;
137
+ expect(typingCount).toBeGreaterThanOrEqual(2);
151
138
  const followupUserIndex = messages.findIndex((message) => {
152
139
  return message.author.id === TEST_USER_ID
153
140
  && message.content.includes('TYPING_REPULSE_MARKER');
@@ -0,0 +1,117 @@
1
+ // E2e test: queued message must drain after the user answers a pending question
2
+ // via the Discord dropdown select menu. Reproduces a bug where answering via
3
+ // select (not text) leaves queued messages stuck because the session continues
4
+ // processing after the answer and may enter another blocking state.
5
+ import { describe, test, expect } from 'vitest';
6
+ import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
7
+ import { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
8
+ import { pendingQuestionContexts } from './commands/ask-question.js';
9
+ const TEXT_CHANNEL_ID = '200000000000001030';
10
+ async function waitForPendingQuestion({ threadId, timeoutMs, }) {
11
+ const start = Date.now();
12
+ while (Date.now() - start < timeoutMs) {
13
+ const entry = [...pendingQuestionContexts.entries()].find(([, context]) => {
14
+ return context.thread.id === threadId;
15
+ });
16
+ if (entry) {
17
+ return { contextHash: entry[0] };
18
+ }
19
+ await new Promise((resolve) => {
20
+ setTimeout(resolve, 100);
21
+ });
22
+ }
23
+ throw new Error('Timed out waiting for pending question context');
24
+ }
25
+ describe('queue drain after question select answer', () => {
26
+ const ctx = setupQueueAdvancedSuite({
27
+ channelId: TEXT_CHANNEL_ID,
28
+ channelName: 'qa-question-select-drain',
29
+ dirName: 'qa-question-select-drain',
30
+ username: 'question-select-tester',
31
+ });
32
+ test('queued message drains after answering question via dropdown select', async () => {
33
+ // 1. Send a message that triggers the question tool
34
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
35
+ content: 'QUESTION_SELECT_QUEUE_MARKER',
36
+ });
37
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
38
+ timeout: 4_000,
39
+ predicate: (t) => {
40
+ return t.name === 'QUESTION_SELECT_QUEUE_MARKER';
41
+ },
42
+ });
43
+ const th = ctx.discord.thread(thread.id);
44
+ // 2. Wait for the question dropdown to appear
45
+ const pending = await waitForPendingQuestion({
46
+ threadId: thread.id,
47
+ timeoutMs: 4_000,
48
+ });
49
+ expect(pending.contextHash).toBeTruthy();
50
+ // Verify dropdown message appeared
51
+ const questionMessages = await waitForBotMessageContaining({
52
+ discord: ctx.discord,
53
+ threadId: thread.id,
54
+ text: 'How to proceed?',
55
+ timeout: 4_000,
56
+ });
57
+ const questionMsg = questionMessages.find((m) => {
58
+ return m.content.includes('How to proceed?');
59
+ });
60
+ expect(questionMsg).toBeTruthy();
61
+ // 3. Queue a message while question is pending
62
+ const { id: queueInteractionId } = await th.user(TEST_USER_ID)
63
+ .runSlashCommand({
64
+ name: 'queue',
65
+ options: [{ name: 'message', type: 3, value: 'Reply with exactly: post-question-drain' }],
66
+ });
67
+ const queueAck = await th.waitForInteractionAck({
68
+ interactionId: queueInteractionId,
69
+ timeout: 4_000,
70
+ });
71
+ if (!queueAck.messageId) {
72
+ throw new Error('Expected /queue response message id');
73
+ }
74
+ // 4. Answer the question via dropdown select (pick first option "Alpha")
75
+ const interaction = await th.user(TEST_USER_ID).selectMenu({
76
+ messageId: questionMsg.id,
77
+ customId: `ask_question:${pending.contextHash}:0`,
78
+ values: ['0'],
79
+ });
80
+ await th.waitForInteractionAck({
81
+ interactionId: interaction.id,
82
+ timeout: 4_000,
83
+ });
84
+ // 5. Queued message should be handed off to OpenCode's own prompt queue
85
+ // after the question reply, so the dispatch indicator appears without
86
+ // waiting for a later natural idle.
87
+ await waitForBotMessageContaining({
88
+ discord: ctx.discord,
89
+ threadId: thread.id,
90
+ text: '» **question-select-tester:** Reply with exactly: post-question-drain',
91
+ timeout: 4_000,
92
+ });
93
+ // 6. Wait for footer from the drained queued message
94
+ await waitForFooterMessage({
95
+ discord: ctx.discord,
96
+ threadId: thread.id,
97
+ timeout: 4_000,
98
+ afterMessageIncludes: '» **question-select-tester:**',
99
+ afterAuthorId: ctx.discord.botUserId,
100
+ });
101
+ const timeline = await th.text({ showInteractions: true });
102
+ expect(timeline).toMatchInlineSnapshot(`
103
+ "--- from: user (question-select-tester)
104
+ QUESTION_SELECT_QUEUE_MARKER
105
+ --- from: assistant (TestBot)
106
+ **Select action**
107
+ How to proceed?
108
+ ✓ _Alpha_
109
+ [user interaction]
110
+ Queued message (position 1)
111
+ [user selects dropdown: 0]
112
+ » **question-select-tester:** Reply with exactly: post-question-drain
113
+ ⬥ ok
114
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
115
+ `);
116
+ }, 20_000);
117
+ });