kimaki 0.26.0 → 0.28.0

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 (302) hide show
  1. package/dist/agent-model.e2e.test.js +250 -164
  2. package/dist/anthropic-account-identity.js +9 -1
  3. package/dist/anthropic-auth-plugin.js +10 -13
  4. package/dist/anthropic-auth-plugin.test.js +17 -0
  5. package/dist/anthropic-auth-state.js +1 -0
  6. package/dist/bash-tool-schema-plugin.js +32 -0
  7. package/dist/bash-tool-schema-plugin.test.js +49 -0
  8. package/dist/bin.js +73 -19
  9. package/dist/cli-commands/maintenance.js +46 -7
  10. package/dist/cli-commands/project.js +11 -27
  11. package/dist/cli-commands/send.js +30 -1
  12. package/dist/cli-commands/session.js +322 -90
  13. package/dist/cli-commands/task.js +34 -15
  14. package/dist/cli-parsing.test.js +83 -4
  15. package/dist/cli-runner.js +44 -8
  16. package/dist/cli-runner.test.js +27 -0
  17. package/dist/cli-send-thread.e2e.test.js +9 -9
  18. package/dist/cli.js +32 -0
  19. package/dist/commands/abort.js +5 -1
  20. package/dist/commands/action-buttons.js +2 -1
  21. package/dist/commands/add-project.js +4 -7
  22. package/dist/commands/agent.js +164 -32
  23. package/dist/commands/ask-question.js +23 -33
  24. package/dist/commands/ask-question.test.js +43 -1
  25. package/dist/commands/btw.js +28 -6
  26. package/dist/commands/file-upload.js +2 -1
  27. package/dist/commands/fork-subagent.js +2 -4
  28. package/dist/commands/fork.js +4 -4
  29. package/dist/commands/last-sessions.js +17 -10
  30. package/dist/commands/login.js +6 -0
  31. package/dist/commands/merge-worktree.js +47 -14
  32. package/dist/commands/model-variant.js +8 -1
  33. package/dist/commands/model.js +16 -9
  34. package/dist/commands/multioauth.js +1 -0
  35. package/dist/commands/new-worktree.js +170 -102
  36. package/dist/commands/queue.js +3 -2
  37. package/dist/commands/resume.js +4 -4
  38. package/dist/commands/session.js +78 -63
  39. package/dist/commands/user-command.js +6 -8
  40. package/dist/commands/worktrees.js +5 -13
  41. package/dist/config.js +16 -1
  42. package/dist/cpu-profiler.js +211 -0
  43. package/dist/cpu-profiler.test.js +160 -0
  44. package/dist/database.js +152 -3
  45. package/dist/db.js +33 -0
  46. package/dist/db.test.js +267 -1
  47. package/dist/default-channel-provisioning.test.js +148 -0
  48. package/dist/discord-bot.js +517 -488
  49. package/dist/discord-command-registration.js +25 -10
  50. package/dist/discord-command-registration.test.js +17 -0
  51. package/dist/discord-utils.js +97 -5
  52. package/dist/discord-utils.test.js +151 -1
  53. package/dist/errors.js +10 -0
  54. package/dist/exec-async.js +17 -5
  55. package/dist/external-directory-permissions.e2e.test.js +11 -7
  56. package/dist/external-directory-project-deny.e2e.test.js +5 -3
  57. package/dist/external-opencode-sync.js +94 -17
  58. package/dist/external-opencode-sync.test.js +206 -0
  59. package/dist/extract-sdk-error-message.test.js +13 -0
  60. package/dist/file-edit-log.js +197 -0
  61. package/dist/file-edit-log.test.js +258 -0
  62. package/dist/format-tables.js +52 -0
  63. package/dist/format-tables.test.js +93 -0
  64. package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
  65. package/dist/gateway-proxy.e2e.test.js +18 -18
  66. package/dist/git-worktree-core.js +182 -17
  67. package/dist/interaction-handler.js +416 -398
  68. package/dist/ipc-tools-plugin.js +68 -3
  69. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
  70. package/dist/kimaki-opencode-plugin.js +9 -3
  71. package/dist/kimaki-workspace-adaptor.js +33 -10
  72. package/dist/logger.js +11 -1
  73. package/dist/markdown.js +3 -1
  74. package/dist/markdown.test.js +11 -3
  75. package/dist/message-formatting.js +181 -51
  76. package/dist/message-formatting.test.js +529 -22
  77. package/dist/message-preprocessing.js +105 -1
  78. package/dist/oauth-rotation-shared.js +10 -0
  79. package/dist/openai-auth-plugin.js +1 -0
  80. package/dist/openai-auth-state.js +1 -0
  81. package/dist/opencode-command.js +28 -0
  82. package/dist/opencode-command.test.js +109 -1
  83. package/dist/opencode-interrupt-plugin.js +4 -0
  84. package/dist/opencode-interrupt-plugin.test.js +24 -0
  85. package/dist/opencode.js +82 -15
  86. package/dist/project-add.test.js +62 -0
  87. package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
  88. package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
  89. package/dist/question-text-before-dropdown.e2e.test.js +52 -0
  90. package/dist/queue-advanced-abort.e2e.test.js +8 -10
  91. package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
  92. package/dist/queue-advanced-e2e-setup.js +78 -5
  93. package/dist/queue-advanced-footer.e2e.test.js +60 -269
  94. package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
  95. package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
  96. package/dist/queue-advanced-question.e2e.test.js +32 -27
  97. package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
  98. package/dist/queue-advanced-typing.e2e.test.js +7 -9
  99. package/dist/queue-delete-message.e2e.test.js +3 -3
  100. package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
  101. package/dist/queue-interrupt-drain.e2e.test.js +122 -12
  102. package/dist/queue-question-select-drain.e2e.test.js +34 -132
  103. package/dist/runtime-lifecycle.e2e.test.js +21 -51
  104. package/dist/schema.js +35 -1
  105. package/dist/session-handler/event-stream-state.js +391 -50
  106. package/dist/session-handler/event-stream-state.test.js +736 -8
  107. package/dist/session-handler/model-utils.js +172 -5
  108. package/dist/session-handler/model-utils.test.js +445 -0
  109. package/dist/session-handler/opencode-session-event-log.js +30 -1
  110. package/dist/session-handler/thread-ingress.test.js +41 -0
  111. package/dist/session-handler/thread-runtime-state.js +2 -0
  112. package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
  113. package/dist/session-handler/thread-session-runtime.js +574 -299
  114. package/dist/session-search.js +123 -4
  115. package/dist/session-search.test.js +248 -1
  116. package/dist/session-sleep.test.js +224 -0
  117. package/dist/session-title-rename.test.js +37 -39
  118. package/dist/sleep.e2e.test.js +494 -0
  119. package/dist/startup-time.e2e.test.js +2 -1
  120. package/dist/store.js +3 -0
  121. package/dist/system-message.js +102 -16
  122. package/dist/system-message.test.js +192 -15
  123. package/dist/task-runner.js +96 -5
  124. package/dist/task-schedule.js +87 -1
  125. package/dist/task-schedule.test.js +149 -1
  126. package/dist/test-utils.js +17 -9
  127. package/dist/thinking-utils.js +10 -0
  128. package/dist/thinking-utils.test.js +35 -0
  129. package/dist/thread-message-queue.e2e.test.js +65 -213
  130. package/dist/undo-redo.e2e.test.js +17 -9
  131. package/dist/voice-attachment.js +23 -1
  132. package/dist/voice-handler.js +21 -4
  133. package/dist/voice-message.e2e.test.js +144 -30
  134. package/dist/voice-routing-input.e2e.test.js +195 -0
  135. package/dist/voice-routing-question.e2e.test.js +125 -0
  136. package/dist/voice.js +28 -8
  137. package/dist/voice.test.js +96 -30
  138. package/dist/worktree-clone-isolation.e2e.test.js +188 -0
  139. package/dist/worktree-lifecycle.e2e.test.js +55 -36
  140. package/dist/worktrees.js +226 -443
  141. package/dist/worktrees.test.js +435 -58
  142. package/dist/xai-auth-plugin.js +1 -0
  143. package/dist/xai-auth-state.js +1 -0
  144. package/package.json +8 -6
  145. package/skills/bump-holocron-consumers/SKILL.md +137 -0
  146. package/skills/critique/SKILL.md +4 -4
  147. package/skills/errore/SKILL.md +60 -218
  148. package/skills/goke/SKILL.md +150 -10
  149. package/skills/holocron/SKILL.md +154 -17
  150. package/skills/npm-package/SKILL.md +55 -35
  151. package/skills/profano/SKILL.md +17 -2
  152. package/skills/spiceflow/SKILL.md +39 -3
  153. package/skills/strada/SKILL.md +39 -0
  154. package/skills/zele/SKILL.md +19 -0
  155. package/src/agent-model.e2e.test.ts +287 -186
  156. package/src/anthropic-account-identity.ts +20 -1
  157. package/src/anthropic-auth-plugin.test.ts +19 -0
  158. package/src/anthropic-auth-plugin.ts +12 -16
  159. package/src/anthropic-auth-state.ts +1 -0
  160. package/src/bash-tool-schema-plugin.test.ts +55 -0
  161. package/src/bash-tool-schema-plugin.ts +46 -0
  162. package/src/bin.ts +78 -26
  163. package/src/cli-commands/maintenance.ts +59 -13
  164. package/src/cli-commands/project.ts +14 -35
  165. package/src/cli-commands/send.ts +31 -1
  166. package/src/cli-commands/session.ts +374 -114
  167. package/src/cli-commands/task.ts +34 -15
  168. package/src/cli-parsing.test.ts +103 -4
  169. package/src/cli-runner.test.ts +49 -0
  170. package/src/cli-runner.ts +44 -7
  171. package/src/cli-send-thread.e2e.test.ts +9 -11
  172. package/src/cli.ts +53 -0
  173. package/src/commands/abort.ts +6 -1
  174. package/src/commands/action-buttons.ts +2 -1
  175. package/src/commands/add-project.ts +4 -8
  176. package/src/commands/agent.ts +205 -29
  177. package/src/commands/ask-question.test.ts +48 -1
  178. package/src/commands/ask-question.ts +23 -33
  179. package/src/commands/btw.ts +40 -5
  180. package/src/commands/file-upload.ts +2 -1
  181. package/src/commands/fork-subagent.ts +2 -3
  182. package/src/commands/fork.ts +4 -3
  183. package/src/commands/last-sessions.ts +10 -4
  184. package/src/commands/login.ts +6 -0
  185. package/src/commands/merge-worktree.ts +54 -16
  186. package/src/commands/model-variant.ts +9 -1
  187. package/src/commands/model.ts +20 -9
  188. package/src/commands/multioauth.ts +1 -0
  189. package/src/commands/new-worktree.ts +138 -55
  190. package/src/commands/queue.ts +3 -2
  191. package/src/commands/resume.ts +4 -3
  192. package/src/commands/session.ts +94 -70
  193. package/src/commands/user-command.ts +6 -6
  194. package/src/commands/worktrees.ts +5 -12
  195. package/src/config.ts +18 -1
  196. package/src/cpu-profiler.test.ts +183 -0
  197. package/src/cpu-profiler.ts +293 -0
  198. package/src/database.ts +235 -3
  199. package/src/db.test.ts +293 -0
  200. package/src/db.ts +38 -0
  201. package/src/default-channel-provisioning.test.ts +155 -0
  202. package/src/discord-bot.ts +78 -36
  203. package/src/discord-command-registration.test.ts +20 -0
  204. package/src/discord-command-registration.ts +42 -12
  205. package/src/discord-utils.test.ts +176 -0
  206. package/src/discord-utils.ts +152 -7
  207. package/src/errors.ts +12 -0
  208. package/src/exec-async.ts +20 -11
  209. package/src/external-directory-permissions.e2e.test.ts +11 -7
  210. package/src/external-directory-project-deny.e2e.test.ts +5 -3
  211. package/src/external-opencode-sync.test.ts +248 -0
  212. package/src/external-opencode-sync.ts +122 -19
  213. package/src/extract-sdk-error-message.test.ts +18 -0
  214. package/src/file-edit-log.test.ts +287 -0
  215. package/src/file-edit-log.ts +283 -0
  216. package/src/format-tables.test.ts +101 -0
  217. package/src/format-tables.ts +75 -0
  218. package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
  219. package/src/gateway-proxy.e2e.test.ts +18 -17
  220. package/src/git-worktree-core.ts +247 -20
  221. package/src/interaction-handler.ts +36 -13
  222. package/src/ipc-tools-plugin.ts +82 -5
  223. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
  224. package/src/kimaki-opencode-plugin.ts +9 -3
  225. package/src/kimaki-workspace-adaptor.ts +40 -11
  226. package/src/logger.ts +14 -4
  227. package/src/markdown.test.ts +13 -3
  228. package/src/markdown.ts +4 -1
  229. package/src/message-formatting.test.ts +672 -25
  230. package/src/message-formatting.ts +242 -53
  231. package/src/message-preprocessing.ts +116 -1
  232. package/src/oauth-rotation-shared.ts +10 -0
  233. package/src/openai-auth-plugin.ts +1 -0
  234. package/src/openai-auth-state.ts +1 -0
  235. package/src/opencode-command.test.ts +140 -0
  236. package/src/opencode-command.ts +38 -0
  237. package/src/opencode-interrupt-plugin.test.ts +32 -0
  238. package/src/opencode-interrupt-plugin.ts +3 -0
  239. package/src/opencode.ts +115 -15
  240. package/src/project-add.test.ts +80 -0
  241. package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
  242. package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
  243. package/src/question-text-before-dropdown.e2e.test.ts +67 -0
  244. package/src/queue-advanced-abort.e2e.test.ts +8 -9
  245. package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
  246. package/src/queue-advanced-e2e-setup.ts +83 -4
  247. package/src/queue-advanced-footer.e2e.test.ts +67 -315
  248. package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
  249. package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
  250. package/src/queue-advanced-question.e2e.test.ts +36 -30
  251. package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
  252. package/src/queue-advanced-typing.e2e.test.ts +8 -8
  253. package/src/queue-delete-message.e2e.test.ts +3 -3
  254. package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
  255. package/src/queue-interrupt-drain.e2e.test.ts +133 -12
  256. package/src/queue-question-select-drain.e2e.test.ts +36 -161
  257. package/src/runtime-lifecycle.e2e.test.ts +21 -54
  258. package/src/schema.sql +15 -2
  259. package/src/schema.ts +37 -1
  260. package/src/session-handler/event-stream-state.test.ts +790 -7
  261. package/src/session-handler/event-stream-state.ts +568 -53
  262. package/src/session-handler/model-utils.test.ts +537 -0
  263. package/src/session-handler/model-utils.ts +281 -5
  264. package/src/session-handler/opencode-session-event-log.ts +31 -1
  265. package/src/session-handler/thread-ingress.test.ts +54 -0
  266. package/src/session-handler/thread-runtime-state.ts +3 -0
  267. package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
  268. package/src/session-handler/thread-session-runtime.ts +712 -358
  269. package/src/session-search.test.ts +281 -0
  270. package/src/session-search.ts +241 -7
  271. package/src/session-sleep.test.ts +294 -0
  272. package/src/session-title-rename.test.ts +54 -48
  273. package/src/sleep.e2e.test.ts +565 -0
  274. package/src/startup-time.e2e.test.ts +5 -1
  275. package/src/store.ts +22 -0
  276. package/src/system-message.test.ts +226 -15
  277. package/src/system-message.ts +130 -16
  278. package/src/task-runner.ts +138 -3
  279. package/src/task-schedule.test.ts +170 -0
  280. package/src/task-schedule.ts +141 -1
  281. package/src/test-utils.ts +21 -10
  282. package/src/thinking-utils.test.ts +46 -0
  283. package/src/thinking-utils.ts +21 -0
  284. package/src/thread-message-queue.e2e.test.ts +65 -245
  285. package/src/undo-redo.e2e.test.ts +17 -9
  286. package/src/voice-attachment.ts +29 -1
  287. package/src/voice-handler.ts +23 -4
  288. package/src/voice-message.e2e.test.ts +149 -29
  289. package/src/voice-routing-input.e2e.test.ts +203 -0
  290. package/src/voice-routing-question.e2e.test.ts +131 -0
  291. package/src/voice.test.ts +107 -29
  292. package/src/voice.ts +34 -6
  293. package/src/worktree-clone-isolation.e2e.test.ts +213 -0
  294. package/src/worktree-lifecycle.e2e.test.ts +55 -35
  295. package/src/worktrees.test.ts +505 -64
  296. package/src/worktrees.ts +257 -603
  297. package/src/xai-auth-plugin.ts +1 -0
  298. package/src/xai-auth-state.ts +1 -0
  299. package/dist/message-finish-field.e2e.test.js +0 -169
  300. package/dist/subagent-rate-limit-plugin.js +0 -184
  301. package/src/message-finish-field.e2e.test.ts +0 -196
  302. package/src/subagent-rate-limit-plugin.ts +0 -228
@@ -9,6 +9,12 @@ import { processImage } from './image-utils.js';
9
9
  import { parsePatchFileCounts } from './patch-text-parser.js';
10
10
  import { getDataDir } from './config.js';
11
11
  const logger = createLogger(LogPrefix.FORMATTING);
12
+ export const TOOL_PREFIX = '┣ ';
13
+ export const FILE_EDIT_PREFIX = '◼︎ ';
14
+ export const THINKING_PREFIX = '┣ ';
15
+ export const STATUS_PREFIX = '⬦ ';
16
+ export const QUEUE_PREFIX = '» ';
17
+ export const WORKTREE_PREFIX = STATUS_PREFIX;
12
18
  /**
13
19
  * Serialize Discord embeds into plain text so the AI model can read them.
14
20
  * Each embed becomes an <embed> XML block with title, author, description,
@@ -137,6 +143,103 @@ function escapeInlineMarkdown(text) {
137
143
  function normalizeWhitespace(text) {
138
144
  return text.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ');
139
145
  }
146
+ export function sessionPartKind(part) {
147
+ return part.type === 'text' ? 'text' : 'tool';
148
+ }
149
+ export function shouldLeadWithBlankLine({ previousKind, nextKind, }) {
150
+ if (!previousKind)
151
+ return false;
152
+ return previousKind !== nextKind;
153
+ }
154
+ export function sessionPartContent({ content, leadWithBlankLine, }) {
155
+ if (!leadWithBlankLine)
156
+ return content;
157
+ return `\n${content}`;
158
+ }
159
+ export function asDiscordQuote(text) {
160
+ const lead = text.startsWith('\n') ? '\n' : '';
161
+ const body = lead ? text.slice(1) : text;
162
+ return lead + body.split('\n').map((line) => {
163
+ if (line.startsWith('>'))
164
+ return line;
165
+ return `> ${line}`;
166
+ }).join('\n');
167
+ }
168
+ function isNonEmptyTextPart(part) {
169
+ return part.type === 'text' && Boolean(part.text?.trim());
170
+ }
171
+ function isRenderableTurnPart(part) {
172
+ return isNonEmptyTextPart(part) || part.type === 'tool';
173
+ }
174
+ function nextToolNameAfter({ parts, fromIndex, }) {
175
+ for (const part of parts.slice(fromIndex + 1)) {
176
+ if (isNonEmptyTextPart(part))
177
+ return undefined;
178
+ if (part.type === 'tool' && part.tool)
179
+ return part.tool;
180
+ }
181
+ }
182
+ export function shouldQuoteIntermediateTextPart({ part, isLastInTurn, nextToolName, }) {
183
+ if (!isNonEmptyTextPart(part) || isLastInTurn)
184
+ return false;
185
+ const text = part.text ?? '';
186
+ if (text.includes('<callout'))
187
+ return false;
188
+ if (text.trim().split('\n').length > 2)
189
+ return false;
190
+ if (nextToolName === 'question'
191
+ || nextToolName?.endsWith('kimaki_sleep')
192
+ || nextToolName?.endsWith('kimaki_action_buttons')) {
193
+ return false;
194
+ }
195
+ return true;
196
+ }
197
+ export function planAssistantTurnFlush({ parts, mode, throughPartId, }) {
198
+ const lastText = parts.filter(isNonEmptyTextPart).at(-1);
199
+ const lastTextIndex = lastText
200
+ ? parts.findLastIndex((part) => part.id === lastText.id)
201
+ : -1;
202
+ const throughIndex = throughPartId
203
+ ? parts.findIndex((part) => part.id === throughPartId)
204
+ : -1;
205
+ const sendUntil = (() => {
206
+ if (mode === 'final')
207
+ return parts.length;
208
+ if (mode === 'interactive') {
209
+ if (throughIndex >= 0)
210
+ return throughIndex + 1;
211
+ return parts.length;
212
+ }
213
+ if (!lastText)
214
+ return parts.length;
215
+ // Hold unfinished last text and anything after it so tool order stays intact.
216
+ if (!lastText.time?.end)
217
+ return lastTextIndex;
218
+ return parts.length;
219
+ })();
220
+ const sendParts = [];
221
+ const holdParts = [];
222
+ for (const [index, part] of parts.entries()) {
223
+ if (index < sendUntil) {
224
+ sendParts.push({
225
+ part,
226
+ quoteText: shouldQuoteIntermediateTextPart({
227
+ part,
228
+ isLastInTurn: mode !== 'progress'
229
+ && parts.filter(isRenderableTurnPart).at(-1)?.id === part.id,
230
+ nextToolName: nextToolNameAfter({ parts, fromIndex: index }),
231
+ }),
232
+ });
233
+ continue;
234
+ }
235
+ holdParts.push(part);
236
+ }
237
+ return {
238
+ send: sendParts.map((entry) => ({ id: entry.part.id, quoteText: entry.quoteText })),
239
+ hold: holdParts.map((part) => ({ id: part.id, quoteText: false })),
240
+ sendParts,
241
+ };
242
+ }
140
243
  /**
141
244
  * Collect renderable assistant parts from session messages as SessionChunks.
142
245
  * Each non-empty formatted part becomes one chunk. Caller can batch them
@@ -161,7 +264,19 @@ export function collectSessionChunks({ messages, skipPartIds, limit, }) {
161
264
  if (!content.trim()) {
162
265
  continue;
163
266
  }
164
- allChunks.push({ partIds: [part.id], content: content.trimEnd() });
267
+ const quote = shouldQuoteIntermediateTextPart({
268
+ part,
269
+ isLastInTurn: false,
270
+ nextToolName: nextToolNameAfter({
271
+ parts: message.parts,
272
+ fromIndex: message.parts.indexOf(part),
273
+ }),
274
+ });
275
+ allChunks.push({
276
+ partIds: [part.id],
277
+ content: (quote ? asDiscordQuote(content) : content).trimEnd(),
278
+ kind: sessionPartKind(part),
279
+ });
165
280
  }
166
281
  }
167
282
  if (limit !== undefined && allChunks.length > limit) {
@@ -180,19 +295,37 @@ export function batchChunksForDiscord(chunks) {
180
295
  return [];
181
296
  }
182
297
  const batched = [];
183
- let current = { partIds: [...chunks[0].partIds], content: chunks[0].content };
298
+ let current = {
299
+ partIds: [...chunks[0].partIds],
300
+ content: chunks[0].content,
301
+ kind: chunks[0].kind,
302
+ };
184
303
  for (let i = 1; i < chunks.length; i++) {
185
304
  const next = chunks[i];
305
+ if (next.kind !== current.kind) {
306
+ batched.push(current);
307
+ current = {
308
+ partIds: [...next.partIds],
309
+ content: next.content,
310
+ kind: next.kind,
311
+ };
312
+ continue;
313
+ }
186
314
  const merged = current.content + '\n' + next.content;
187
315
  if (merged.length <= DISCORD_BATCH_MAX_LENGTH) {
188
316
  current = {
189
317
  partIds: [...current.partIds, ...next.partIds],
190
318
  content: merged,
319
+ kind: current.kind,
191
320
  };
192
321
  }
193
322
  else {
194
323
  batched.push(current);
195
- current = { partIds: [...next.partIds], content: next.content };
324
+ current = {
325
+ partIds: [...next.partIds],
326
+ content: next.content,
327
+ kind: next.kind,
328
+ };
196
329
  }
197
330
  }
198
331
  batched.push(current);
@@ -323,22 +456,12 @@ export async function getFileAttachments(message) {
323
456
  }));
324
457
  return results.filter((r) => r !== null);
325
458
  }
326
- const MAX_BASH_COMMAND_INLINE_LENGTH = 100;
327
- /**
328
- * Format the inline title for a bash tool part. Handles three cases:
329
- * 1. Short single-line command → show full command
330
- * 2. Long/multiline command with description → show description
331
- * 3. Long/multiline command without description → truncate first line of command
332
- *
333
- * The description field was removed from the opencode v2 bash tool schema but
334
- * kimaki's system prompt instructs models to always send it as an extra field.
335
- * Case 3 is the fallback when a model omits it.
336
- */
337
- export function formatBashToolTitle({ command, description, stateTitle, }) {
338
- if (!command && !description && !stateTitle)
459
+ const MAX_BASH_COMMAND_INLINE_LENGTH = 50;
460
+ export function formatBashToolTitle({ command, description, summary, stateTitle, }) {
461
+ const label = description || summary;
462
+ if (!command && !label && !stateTitle)
339
463
  return '';
340
464
  const isSingleLine = !command.includes('\n');
341
- // Find first non-empty line to handle commands with leading blank lines
342
465
  const firstMeaningfulLine = command
343
466
  .split('\n')
344
467
  .find((line) => line.trim().length > 0)
@@ -346,8 +469,8 @@ export function formatBashToolTitle({ command, description, stateTitle, }) {
346
469
  if (command && isSingleLine && command.length <= MAX_BASH_COMMAND_INLINE_LENGTH) {
347
470
  return ` _${escapeInlineMarkdown(command)}_`;
348
471
  }
349
- if (description) {
350
- return ` _${escapeInlineMarkdown(description)}_`;
472
+ if (label) {
473
+ return ` _${escapeInlineMarkdown(label)}_`;
351
474
  }
352
475
  if (firstMeaningfulLine.length > 0) {
353
476
  const needsTruncation = firstMeaningfulLine.length > MAX_BASH_COMMAND_INLINE_LENGTH;
@@ -444,6 +567,21 @@ export function getToolSummaryText(part) {
444
567
  const prompt = part.state.input?.prompt || '';
445
568
  return prompt ? `*${escapeInlineMarkdown(prompt.slice(0, 60))}*` : '';
446
569
  }
570
+ // LESSON: tool parts are sent to Discord at `running` status (see
571
+ // shouldSendPart in thread-session-runtime), so `state.title` from the tool
572
+ // result is never rendered here. Everything shown must come from state.input.
573
+ // `until` is already absolute; `duration` is relative, so say "for 2h" —
574
+ // never "until 2h".
575
+ if (part.tool.endsWith('kimaki_sleep')) {
576
+ const until = part.state.input?.until || '';
577
+ const duration = part.state.input?.duration || '';
578
+ const reason = part.state.input?.reason || '';
579
+ const when = until ? `until ${until}` : duration ? `for ${duration}` : '';
580
+ const reasonText = reason ? `_${escapeInlineMarkdown(reason)}_` : '';
581
+ return [when && escapeInlineMarkdown(when), reasonText]
582
+ .filter(Boolean)
583
+ .join(' ');
584
+ }
447
585
  if (!part.state.input)
448
586
  return '';
449
587
  const inputFields = Object.entries(part.state.input)
@@ -470,23 +608,16 @@ export function formatTodoList(part) {
470
608
  const activeTodo = todos[activeIndex];
471
609
  if (activeIndex === -1 || !activeTodo)
472
610
  return '';
473
- // digit-with-period ⒈-⒛ for 1-20, fallback to regular number for 21+
474
- const digitWithPeriod = '⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛';
475
611
  const todoNumber = activeIndex + 1;
476
- const num = todoNumber <= 20 ? digitWithPeriod[todoNumber - 1] : `${todoNumber}.`;
477
612
  const content = activeTodo.content.charAt(0).toLowerCase() + activeTodo.content.slice(1);
478
- return `${num} **${escapeInlineMarkdown(content)}**`;
613
+ return `${todoNumber}. **${escapeInlineMarkdown(content)}**`;
479
614
  }
480
615
  export function formatTaskToolTitle(part) {
481
- if (part.tool !== 'task' || part.state.status === 'pending')
482
- return '';
483
- const childSessionId = part.state.metadata?.sessionId;
484
- if (typeof childSessionId !== 'string' || !childSessionId)
616
+ // Running only. The child session can be created later when many tasks queue.
617
+ if (part.tool !== 'task' || part.state.status !== 'running')
485
618
  return '';
486
619
  const description = part.state.input?.description;
487
- const stateTitle = part.state.status === 'running' || part.state.status === 'completed'
488
- ? part.state.title
489
- : undefined;
620
+ const stateTitle = part.state.title;
490
621
  const title = typeof description === 'string' && description
491
622
  ? description
492
623
  : typeof stateTitle === 'string'
@@ -496,7 +627,7 @@ export function formatTaskToolTitle(part) {
496
627
  return '';
497
628
  const subagentType = part.state.input?.subagent_type;
498
629
  const agent = typeof subagentType === 'string' ? subagentType : 'task';
499
- return `┣ ${escapeInlineMarkdown(agent)} **${escapeInlineMarkdown(title)}**`;
630
+ return `${TOOL_PREFIX}${escapeInlineMarkdown(agent)} **${escapeInlineMarkdown(title)}**`;
500
631
  }
501
632
  export function formatPart(part, prefix) {
502
633
  const pfx = prefix ? `${prefix} ⋅ ` : '';
@@ -504,24 +635,15 @@ export function formatPart(part, prefix) {
504
635
  const text = part.text?.trim();
505
636
  if (!text)
506
637
  return '';
507
- // For subtask text, always use bullet with prefix
508
638
  if (prefix) {
509
- return `⬥ ${pfx}${text}`;
639
+ return `${pfx}${text}`;
510
640
  }
511
- const firstChar = text[0] || '';
512
- const markdownStarters = ['#', '*', '_', '-', '>', '`', '[', '|'];
513
- const startsWithMarkdown = markdownStarters.includes(firstChar) ||
514
- /^\d+\./.test(text) ||
515
- /^<callout[\s>]/i.test(text);
516
- if (startsWithMarkdown) {
517
- return `\n${text}`;
518
- }
519
- return `⬥ ${text}`;
641
+ return text;
520
642
  }
521
643
  if (part.type === 'reasoning') {
522
644
  if (!part.text?.trim())
523
645
  return '';
524
- return `┣ ${pfx}thinking`;
646
+ return `${THINKING_PREFIX}${pfx}thinking`;
525
647
  }
526
648
  if (part.type === 'file') {
527
649
  return prefix
@@ -530,19 +652,20 @@ export function formatPart(part, prefix) {
530
652
  }
531
653
  if (part.type === 'step-start' ||
532
654
  part.type === 'step-finish' ||
533
- part.type === 'patch') {
655
+ part.type === 'patch' ||
656
+ part.type === 'compaction') {
534
657
  return '';
535
658
  }
536
659
  if (part.type === 'agent') {
537
- return `┣ ${pfx}agent ${part.id}`;
660
+ return `${TOOL_PREFIX}${pfx}agent ${part.id}`;
538
661
  }
539
662
  if (part.type === 'snapshot') {
540
- return `┣ ${pfx}snapshot ${part.snapshot}`;
663
+ return `${TOOL_PREFIX}${pfx}snapshot ${part.snapshot}`;
541
664
  }
542
665
  if (part.type === 'tool') {
543
666
  if (part.tool === 'todowrite') {
544
667
  const formatted = formatTodoList(part);
545
- return prefix && formatted ? `┣ ${pfx}${formatted}` : formatted;
668
+ return prefix && formatted ? `${TOOL_PREFIX}${pfx}${formatted}` : formatted;
546
669
  }
547
670
  // Question tool is handled via Discord dropdowns, not text
548
671
  if (part.tool === 'question') {
@@ -566,11 +689,13 @@ export function formatPart(part, prefix) {
566
689
  }
567
690
  const command = part.state.input?.command || '';
568
691
  const description = part.state.input?.description || '';
692
+ const summary = part.state.input?.summary || '';
569
693
  const toolTitle = formatBashToolTitle({
570
694
  command,
571
695
  description,
696
+ summary,
572
697
  });
573
- return `┣ ${pfx}bash${toolTitle}`;
698
+ return `${TOOL_PREFIX}${pfx}bash${toolTitle}`;
574
699
  }
575
700
  const summaryText = getToolSummaryText(part);
576
701
  const stateTitle = 'title' in part.state ? part.state.title : undefined;
@@ -581,9 +706,11 @@ export function formatPart(part, prefix) {
581
706
  else if (part.tool === 'bash') {
582
707
  const command = part.state.input?.command || '';
583
708
  const description = part.state.input?.description || '';
709
+ const summary = part.state.input?.summary || '';
584
710
  const formatted = formatBashToolTitle({
585
711
  command,
586
712
  description,
713
+ summary,
587
714
  stateTitle,
588
715
  });
589
716
  toolTitle = formatted.startsWith(' ') ? formatted.slice(1) : formatted;
@@ -598,14 +725,17 @@ export function formatPart(part, prefix) {
598
725
  if (part.tool === 'edit' ||
599
726
  part.tool === 'write' ||
600
727
  part.tool === 'apply_patch') {
601
- return '◼︎';
728
+ return FILE_EDIT_PREFIX;
602
729
  }
603
- return '┣';
730
+ return TOOL_PREFIX;
604
731
  })();
605
732
  const toolParts = [part.tool, toolTitle, summaryText]
606
733
  .filter(Boolean)
607
734
  .join(' ');
608
- return `${icon} ${pfx}${toolParts}`;
735
+ if (icon === '⨯') {
736
+ return `${icon} ${pfx}${toolParts}`;
737
+ }
738
+ return `${icon}${pfx}${toolParts}`;
609
739
  }
610
740
  logger.warn('Unknown part type:', part);
611
741
  return '';