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.
- package/dist/agent-model.e2e.test.js +250 -164
- package/dist/anthropic-account-identity.js +9 -1
- package/dist/anthropic-auth-plugin.js +10 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +30 -1
- package/dist/cli-commands/session.js +322 -90
- package/dist/cli-commands/task.js +34 -15
- package/dist/cli-parsing.test.js +83 -4
- package/dist/cli-runner.js +44 -8
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +164 -32
- package/dist/commands/ask-question.js +23 -33
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/btw.js +28 -6
- package/dist/commands/file-upload.js +2 -1
- package/dist/commands/fork-subagent.js +2 -4
- package/dist/commands/fork.js +4 -4
- package/dist/commands/last-sessions.js +17 -10
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +47 -14
- package/dist/commands/model-variant.js +8 -1
- package/dist/commands/model.js +16 -9
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +152 -3
- package/dist/db.js +33 -0
- package/dist/db.test.js +267 -1
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +517 -488
- package/dist/discord-command-registration.js +25 -10
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +97 -5
- package/dist/discord-utils.test.js +151 -1
- package/dist/errors.js +10 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +11 -7
- package/dist/external-directory-project-deny.e2e.test.js +5 -3
- package/dist/external-opencode-sync.js +94 -17
- package/dist/external-opencode-sync.test.js +206 -0
- package/dist/extract-sdk-error-message.test.js +13 -0
- package/dist/file-edit-log.js +197 -0
- package/dist/file-edit-log.test.js +258 -0
- package/dist/format-tables.js +52 -0
- package/dist/format-tables.test.js +93 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/git-worktree-core.js +182 -17
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +68 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +9 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +11 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +181 -51
- package/dist/message-formatting.test.js +529 -22
- package/dist/message-preprocessing.js +105 -1
- package/dist/oauth-rotation-shared.js +10 -0
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/openai-auth-state.js +1 -0
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +109 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +82 -15
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +8 -10
- package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +60 -269
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
- package/dist/queue-advanced-question.e2e.test.js +32 -27
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
- package/dist/queue-advanced-typing.e2e.test.js +7 -9
- package/dist/queue-delete-message.e2e.test.js +3 -3
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
- package/dist/queue-interrupt-drain.e2e.test.js +122 -12
- package/dist/queue-question-select-drain.e2e.test.js +34 -132
- package/dist/runtime-lifecycle.e2e.test.js +21 -51
- package/dist/schema.js +35 -1
- package/dist/session-handler/event-stream-state.js +391 -50
- package/dist/session-handler/event-stream-state.test.js +736 -8
- package/dist/session-handler/model-utils.js +172 -5
- package/dist/session-handler/model-utils.test.js +445 -0
- package/dist/session-handler/opencode-session-event-log.js +30 -1
- package/dist/session-handler/thread-ingress.test.js +41 -0
- package/dist/session-handler/thread-runtime-state.js +2 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +574 -299
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +37 -39
- package/dist/sleep.e2e.test.js +494 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +102 -16
- package/dist/system-message.test.js +192 -15
- package/dist/task-runner.js +96 -5
- package/dist/task-schedule.js +87 -1
- package/dist/task-schedule.test.js +149 -1
- package/dist/test-utils.js +17 -9
- package/dist/thinking-utils.js +10 -0
- package/dist/thinking-utils.test.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +65 -213
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-handler.js +21 -4
- package/dist/voice-message.e2e.test.js +144 -30
- package/dist/voice-routing-input.e2e.test.js +195 -0
- package/dist/voice-routing-question.e2e.test.js +125 -0
- package/dist/voice.js +28 -8
- package/dist/voice.test.js +96 -30
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +55 -36
- package/dist/worktrees.js +226 -443
- package/dist/worktrees.test.js +435 -58
- package/dist/xai-auth-plugin.js +1 -0
- package/dist/xai-auth-state.js +1 -0
- package/package.json +8 -6
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +60 -218
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +154 -17
- package/skills/npm-package/SKILL.md +55 -35
- package/skills/profano/SKILL.md +17 -2
- package/skills/spiceflow/SKILL.md +39 -3
- package/skills/strada/SKILL.md +39 -0
- package/skills/zele/SKILL.md +19 -0
- package/src/agent-model.e2e.test.ts +287 -186
- package/src/anthropic-account-identity.ts +20 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +12 -16
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +31 -1
- package/src/cli-commands/session.ts +374 -114
- package/src/cli-commands/task.ts +34 -15
- package/src/cli-parsing.test.ts +103 -4
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +44 -7
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +205 -29
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +23 -33
- package/src/commands/btw.ts +40 -5
- package/src/commands/file-upload.ts +2 -1
- package/src/commands/fork-subagent.ts +2 -3
- package/src/commands/fork.ts +4 -3
- package/src/commands/last-sessions.ts +10 -4
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +54 -16
- package/src/commands/model-variant.ts +9 -1
- package/src/commands/model.ts +20 -9
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +235 -3
- package/src/db.test.ts +293 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +78 -36
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +42 -12
- package/src/discord-utils.test.ts +176 -0
- package/src/discord-utils.ts +152 -7
- package/src/errors.ts +12 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +11 -7
- package/src/external-directory-project-deny.e2e.test.ts +5 -3
- package/src/external-opencode-sync.test.ts +248 -0
- package/src/external-opencode-sync.ts +122 -19
- package/src/extract-sdk-error-message.test.ts +18 -0
- package/src/file-edit-log.test.ts +287 -0
- package/src/file-edit-log.ts +283 -0
- package/src/format-tables.test.ts +101 -0
- package/src/format-tables.ts +75 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/git-worktree-core.ts +247 -20
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +82 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +9 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +14 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +672 -25
- package/src/message-formatting.ts +242 -53
- package/src/message-preprocessing.ts +116 -1
- package/src/oauth-rotation-shared.ts +10 -0
- package/src/openai-auth-plugin.ts +1 -0
- package/src/openai-auth-state.ts +1 -0
- package/src/opencode-command.test.ts +140 -0
- package/src/opencode-command.ts +38 -0
- package/src/opencode-interrupt-plugin.test.ts +32 -0
- package/src/opencode-interrupt-plugin.ts +3 -0
- package/src/opencode.ts +115 -15
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +8 -9
- package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +67 -315
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
- package/src/queue-advanced-question.e2e.test.ts +36 -30
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
- package/src/queue-advanced-typing.e2e.test.ts +8 -8
- package/src/queue-delete-message.e2e.test.ts +3 -3
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
- package/src/queue-interrupt-drain.e2e.test.ts +133 -12
- package/src/queue-question-select-drain.e2e.test.ts +36 -161
- package/src/runtime-lifecycle.e2e.test.ts +21 -54
- package/src/schema.sql +15 -2
- package/src/schema.ts +37 -1
- package/src/session-handler/event-stream-state.test.ts +790 -7
- package/src/session-handler/event-stream-state.ts +568 -53
- package/src/session-handler/model-utils.test.ts +537 -0
- package/src/session-handler/model-utils.ts +281 -5
- package/src/session-handler/opencode-session-event-log.ts +31 -1
- package/src/session-handler/thread-ingress.test.ts +54 -0
- package/src/session-handler/thread-runtime-state.ts +3 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +712 -358
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +54 -48
- package/src/sleep.e2e.test.ts +565 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +22 -0
- package/src/system-message.test.ts +226 -15
- package/src/system-message.ts +130 -16
- package/src/task-runner.ts +138 -3
- package/src/task-schedule.test.ts +170 -0
- package/src/task-schedule.ts +141 -1
- package/src/test-utils.ts +21 -10
- package/src/thinking-utils.test.ts +46 -0
- package/src/thinking-utils.ts +21 -0
- package/src/thread-message-queue.e2e.test.ts +65 -245
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-attachment.ts +29 -1
- package/src/voice-handler.ts +23 -4
- package/src/voice-message.e2e.test.ts +149 -29
- package/src/voice-routing-input.e2e.test.ts +203 -0
- package/src/voice-routing-question.e2e.test.ts +131 -0
- package/src/voice.test.ts +107 -29
- package/src/voice.ts +34 -6
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +55 -35
- package/src/worktrees.test.ts +505 -64
- package/src/worktrees.ts +257 -603
- package/src/xai-auth-plugin.ts +1 -0
- package/src/xai-auth-state.ts +1 -0
- package/dist/message-finish-field.e2e.test.js +0 -169
- package/dist/subagent-rate-limit-plugin.js +0 -184
- package/src/message-finish-field.e2e.test.ts +0 -196
- package/src/subagent-rate-limit-plugin.ts +0 -228
|
@@ -20,12 +20,19 @@ import { getDataDir } from './config.js'
|
|
|
20
20
|
|
|
21
21
|
// Generic message type compatible with both v1 and v2 SDK
|
|
22
22
|
type GenericSessionMessage = {
|
|
23
|
-
info: { role: string; id?: string }
|
|
23
|
+
info: { role: string; id?: string; parentID?: string }
|
|
24
24
|
parts: Part[]
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const logger = createLogger(LogPrefix.FORMATTING)
|
|
28
28
|
|
|
29
|
+
export const TOOL_PREFIX = '┣ '
|
|
30
|
+
export const FILE_EDIT_PREFIX = '◼︎ '
|
|
31
|
+
export const THINKING_PREFIX = '┣ '
|
|
32
|
+
export const STATUS_PREFIX = '⬦ '
|
|
33
|
+
export const QUEUE_PREFIX = '» '
|
|
34
|
+
export const WORKTREE_PREFIX = STATUS_PREFIX
|
|
35
|
+
|
|
29
36
|
/**
|
|
30
37
|
* Serialize Discord embeds into plain text so the AI model can read them.
|
|
31
38
|
* Each embed becomes an <embed> XML block with title, author, description,
|
|
@@ -166,11 +173,165 @@ function normalizeWhitespace(text: string): string {
|
|
|
166
173
|
return text.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ')
|
|
167
174
|
}
|
|
168
175
|
|
|
176
|
+
export type SessionPartKind = 'text' | 'tool'
|
|
177
|
+
|
|
178
|
+
export function sessionPartKind(part: { type: string }): SessionPartKind {
|
|
179
|
+
return part.type === 'text' ? 'text' : 'tool'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function shouldLeadWithBlankLine({
|
|
183
|
+
previousKind,
|
|
184
|
+
nextKind,
|
|
185
|
+
}: {
|
|
186
|
+
previousKind: SessionPartKind | undefined
|
|
187
|
+
nextKind: SessionPartKind
|
|
188
|
+
}): boolean {
|
|
189
|
+
if (!previousKind) return false
|
|
190
|
+
return previousKind !== nextKind
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function sessionPartContent({
|
|
194
|
+
content,
|
|
195
|
+
leadWithBlankLine,
|
|
196
|
+
}: {
|
|
197
|
+
content: string
|
|
198
|
+
leadWithBlankLine: boolean
|
|
199
|
+
}): string {
|
|
200
|
+
if (!leadWithBlankLine) return content
|
|
201
|
+
return `\n${content}`
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function asDiscordQuote(text: string): string {
|
|
205
|
+
const lead = text.startsWith('\n') ? '\n' : ''
|
|
206
|
+
const body = lead ? text.slice(1) : text
|
|
207
|
+
return lead + body.split('\n').map((line) => {
|
|
208
|
+
if (line.startsWith('>')) return line
|
|
209
|
+
return `> ${line}`
|
|
210
|
+
}).join('\n')
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isNonEmptyTextPart(part: { type: string; text?: string }): boolean {
|
|
214
|
+
return part.type === 'text' && Boolean(part.text?.trim())
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isRenderableTurnPart(part: { type: string; text?: string }): boolean {
|
|
218
|
+
return isNonEmptyTextPart(part) || part.type === 'tool'
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function nextToolNameAfter({
|
|
222
|
+
parts,
|
|
223
|
+
fromIndex,
|
|
224
|
+
}: {
|
|
225
|
+
parts: Array<{ type: string; tool?: string; text?: string }>
|
|
226
|
+
fromIndex: number
|
|
227
|
+
}): string | undefined {
|
|
228
|
+
for (const part of parts.slice(fromIndex + 1)) {
|
|
229
|
+
if (isNonEmptyTextPart(part)) return undefined
|
|
230
|
+
if (part.type === 'tool' && part.tool) return part.tool
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function shouldQuoteIntermediateTextPart({
|
|
235
|
+
part,
|
|
236
|
+
isLastInTurn,
|
|
237
|
+
nextToolName,
|
|
238
|
+
}: {
|
|
239
|
+
part: { type: string; text?: string }
|
|
240
|
+
isLastInTurn: boolean
|
|
241
|
+
nextToolName?: string
|
|
242
|
+
}): boolean {
|
|
243
|
+
if (!isNonEmptyTextPart(part) || isLastInTurn) return false
|
|
244
|
+
const text = part.text ?? ''
|
|
245
|
+
if (text.includes('<callout')) return false
|
|
246
|
+
if (text.trim().split('\n').length > 2) return false
|
|
247
|
+
if (
|
|
248
|
+
nextToolName === 'question'
|
|
249
|
+
|| nextToolName?.endsWith('kimaki_sleep')
|
|
250
|
+
|| nextToolName?.endsWith('kimaki_action_buttons')
|
|
251
|
+
) {
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
return true
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export type AssistantTurnFlushMode = 'progress' | 'interactive' | 'final'
|
|
258
|
+
|
|
259
|
+
export type PlannedAssistantTurnPart<T extends { id: string; type: string; text?: string }> = {
|
|
260
|
+
part: T
|
|
261
|
+
quoteText: boolean
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function planAssistantTurnFlush<T extends {
|
|
265
|
+
id: string
|
|
266
|
+
type: string
|
|
267
|
+
text?: string
|
|
268
|
+
tool?: string
|
|
269
|
+
time?: { end?: number; created?: number }
|
|
270
|
+
}>({
|
|
271
|
+
parts,
|
|
272
|
+
mode,
|
|
273
|
+
throughPartId,
|
|
274
|
+
}: {
|
|
275
|
+
parts: T[]
|
|
276
|
+
mode: AssistantTurnFlushMode
|
|
277
|
+
throughPartId?: string
|
|
278
|
+
}): {
|
|
279
|
+
send: Array<{ id: string; quoteText: boolean }>
|
|
280
|
+
hold: Array<{ id: string; quoteText: boolean }>
|
|
281
|
+
sendParts: Array<PlannedAssistantTurnPart<T>>
|
|
282
|
+
} {
|
|
283
|
+
const lastText = parts.filter(isNonEmptyTextPart).at(-1)
|
|
284
|
+
const lastTextIndex = lastText
|
|
285
|
+
? parts.findLastIndex((part) => part.id === lastText.id)
|
|
286
|
+
: -1
|
|
287
|
+
const throughIndex = throughPartId
|
|
288
|
+
? parts.findIndex((part) => part.id === throughPartId)
|
|
289
|
+
: -1
|
|
290
|
+
|
|
291
|
+
const sendUntil = (() => {
|
|
292
|
+
if (mode === 'final') return parts.length
|
|
293
|
+
if (mode === 'interactive') {
|
|
294
|
+
if (throughIndex >= 0) return throughIndex + 1
|
|
295
|
+
return parts.length
|
|
296
|
+
}
|
|
297
|
+
if (!lastText) return parts.length
|
|
298
|
+
// Hold unfinished last text and anything after it so tool order stays intact.
|
|
299
|
+
if (!lastText.time?.end) return lastTextIndex
|
|
300
|
+
return parts.length
|
|
301
|
+
})()
|
|
302
|
+
|
|
303
|
+
const sendParts: Array<PlannedAssistantTurnPart<T>> = []
|
|
304
|
+
const holdParts: T[] = []
|
|
305
|
+
for (const [index, part] of parts.entries()) {
|
|
306
|
+
if (index < sendUntil) {
|
|
307
|
+
sendParts.push({
|
|
308
|
+
part,
|
|
309
|
+
quoteText: shouldQuoteIntermediateTextPart({
|
|
310
|
+
part,
|
|
311
|
+
isLastInTurn:
|
|
312
|
+
mode !== 'progress'
|
|
313
|
+
&& parts.filter(isRenderableTurnPart).at(-1)?.id === part.id,
|
|
314
|
+
nextToolName: nextToolNameAfter({ parts, fromIndex: index }),
|
|
315
|
+
}),
|
|
316
|
+
})
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
319
|
+
holdParts.push(part)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
send: sendParts.map((entry) => ({ id: entry.part.id, quoteText: entry.quoteText })),
|
|
324
|
+
hold: holdParts.map((part) => ({ id: part.id, quoteText: false })),
|
|
325
|
+
sendParts,
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
169
329
|
// A chunk of formatted content with associated part IDs, ready to be
|
|
170
330
|
// batched into as few Discord messages as possible.
|
|
171
331
|
export type SessionChunk = {
|
|
172
332
|
partIds: string[]
|
|
173
333
|
content: string
|
|
334
|
+
kind: SessionPartKind
|
|
174
335
|
}
|
|
175
336
|
|
|
176
337
|
/**
|
|
@@ -206,7 +367,19 @@ export function collectSessionChunks({
|
|
|
206
367
|
if (!content.trim()) {
|
|
207
368
|
continue
|
|
208
369
|
}
|
|
209
|
-
|
|
370
|
+
const quote = shouldQuoteIntermediateTextPart({
|
|
371
|
+
part,
|
|
372
|
+
isLastInTurn: false,
|
|
373
|
+
nextToolName: nextToolNameAfter({
|
|
374
|
+
parts: message.parts,
|
|
375
|
+
fromIndex: message.parts.indexOf(part),
|
|
376
|
+
}),
|
|
377
|
+
})
|
|
378
|
+
allChunks.push({
|
|
379
|
+
partIds: [part.id],
|
|
380
|
+
content: (quote ? asDiscordQuote(content) : content).trimEnd(),
|
|
381
|
+
kind: sessionPartKind(part),
|
|
382
|
+
})
|
|
210
383
|
}
|
|
211
384
|
}
|
|
212
385
|
|
|
@@ -228,19 +401,37 @@ export function batchChunksForDiscord(chunks: SessionChunk[]): SessionChunk[] {
|
|
|
228
401
|
return []
|
|
229
402
|
}
|
|
230
403
|
const batched: SessionChunk[] = []
|
|
231
|
-
let current: SessionChunk = {
|
|
404
|
+
let current: SessionChunk = {
|
|
405
|
+
partIds: [...chunks[0]!.partIds],
|
|
406
|
+
content: chunks[0]!.content,
|
|
407
|
+
kind: chunks[0]!.kind,
|
|
408
|
+
}
|
|
232
409
|
|
|
233
410
|
for (let i = 1; i < chunks.length; i++) {
|
|
234
411
|
const next = chunks[i]!
|
|
412
|
+
if (next.kind !== current.kind) {
|
|
413
|
+
batched.push(current)
|
|
414
|
+
current = {
|
|
415
|
+
partIds: [...next.partIds],
|
|
416
|
+
content: next.content,
|
|
417
|
+
kind: next.kind,
|
|
418
|
+
}
|
|
419
|
+
continue
|
|
420
|
+
}
|
|
235
421
|
const merged = current.content + '\n' + next.content
|
|
236
422
|
if (merged.length <= DISCORD_BATCH_MAX_LENGTH) {
|
|
237
423
|
current = {
|
|
238
424
|
partIds: [...current.partIds, ...next.partIds],
|
|
239
425
|
content: merged,
|
|
426
|
+
kind: current.kind,
|
|
240
427
|
}
|
|
241
428
|
} else {
|
|
242
429
|
batched.push(current)
|
|
243
|
-
current = {
|
|
430
|
+
current = {
|
|
431
|
+
partIds: [...next.partIds],
|
|
432
|
+
content: next.content,
|
|
433
|
+
kind: next.kind,
|
|
434
|
+
}
|
|
244
435
|
}
|
|
245
436
|
}
|
|
246
437
|
batched.push(current)
|
|
@@ -419,31 +610,23 @@ export async function getFileAttachments(
|
|
|
419
610
|
return results.filter((r) => r !== null) as DiscordFileAttachment[]
|
|
420
611
|
}
|
|
421
612
|
|
|
422
|
-
const MAX_BASH_COMMAND_INLINE_LENGTH =
|
|
613
|
+
const MAX_BASH_COMMAND_INLINE_LENGTH = 50
|
|
423
614
|
|
|
424
|
-
/**
|
|
425
|
-
* Format the inline title for a bash tool part. Handles three cases:
|
|
426
|
-
* 1. Short single-line command → show full command
|
|
427
|
-
* 2. Long/multiline command with description → show description
|
|
428
|
-
* 3. Long/multiline command without description → truncate first line of command
|
|
429
|
-
*
|
|
430
|
-
* The description field was removed from the opencode v2 bash tool schema but
|
|
431
|
-
* kimaki's system prompt instructs models to always send it as an extra field.
|
|
432
|
-
* Case 3 is the fallback when a model omits it.
|
|
433
|
-
*/
|
|
434
615
|
export function formatBashToolTitle({
|
|
435
616
|
command,
|
|
436
617
|
description,
|
|
618
|
+
summary,
|
|
437
619
|
stateTitle,
|
|
438
620
|
}: {
|
|
439
621
|
command: string
|
|
440
622
|
description?: string
|
|
623
|
+
summary?: string
|
|
441
624
|
stateTitle?: string
|
|
442
625
|
}): string {
|
|
443
|
-
|
|
626
|
+
const label = description || summary
|
|
627
|
+
if (!command && !label && !stateTitle) return ''
|
|
444
628
|
|
|
445
629
|
const isSingleLine = !command.includes('\n')
|
|
446
|
-
// Find first non-empty line to handle commands with leading blank lines
|
|
447
630
|
const firstMeaningfulLine =
|
|
448
631
|
command
|
|
449
632
|
.split('\n')
|
|
@@ -453,8 +636,8 @@ export function formatBashToolTitle({
|
|
|
453
636
|
if (command && isSingleLine && command.length <= MAX_BASH_COMMAND_INLINE_LENGTH) {
|
|
454
637
|
return ` _${escapeInlineMarkdown(command)}_`
|
|
455
638
|
}
|
|
456
|
-
if (
|
|
457
|
-
return ` _${escapeInlineMarkdown(
|
|
639
|
+
if (label) {
|
|
640
|
+
return ` _${escapeInlineMarkdown(label)}_`
|
|
458
641
|
}
|
|
459
642
|
if (firstMeaningfulLine.length > 0) {
|
|
460
643
|
const needsTruncation = firstMeaningfulLine.length > MAX_BASH_COMMAND_INLINE_LENGTH
|
|
@@ -566,6 +749,22 @@ export function getToolSummaryText(part: Part): string {
|
|
|
566
749
|
return prompt ? `*${escapeInlineMarkdown(prompt.slice(0, 60))}*` : ''
|
|
567
750
|
}
|
|
568
751
|
|
|
752
|
+
// LESSON: tool parts are sent to Discord at `running` status (see
|
|
753
|
+
// shouldSendPart in thread-session-runtime), so `state.title` from the tool
|
|
754
|
+
// result is never rendered here. Everything shown must come from state.input.
|
|
755
|
+
// `until` is already absolute; `duration` is relative, so say "for 2h" —
|
|
756
|
+
// never "until 2h".
|
|
757
|
+
if (part.tool.endsWith('kimaki_sleep')) {
|
|
758
|
+
const until = (part.state.input?.until as string) || ''
|
|
759
|
+
const duration = (part.state.input?.duration as string) || ''
|
|
760
|
+
const reason = (part.state.input?.reason as string) || ''
|
|
761
|
+
const when = until ? `until ${until}` : duration ? `for ${duration}` : ''
|
|
762
|
+
const reasonText = reason ? `_${escapeInlineMarkdown(reason)}_` : ''
|
|
763
|
+
return [when && escapeInlineMarkdown(when), reasonText]
|
|
764
|
+
.filter(Boolean)
|
|
765
|
+
.join(' ')
|
|
766
|
+
}
|
|
767
|
+
|
|
569
768
|
if (!part.state.input) return ''
|
|
570
769
|
|
|
571
770
|
const inputFields = Object.entries(part.state.input)
|
|
@@ -597,26 +796,18 @@ export function formatTodoList(part: Part): string {
|
|
|
597
796
|
})
|
|
598
797
|
const activeTodo = todos[activeIndex]
|
|
599
798
|
if (activeIndex === -1 || !activeTodo) return ''
|
|
600
|
-
// digit-with-period ⒈-⒛ for 1-20, fallback to regular number for 21+
|
|
601
|
-
const digitWithPeriod = '⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛'
|
|
602
799
|
const todoNumber = activeIndex + 1
|
|
603
|
-
const num =
|
|
604
|
-
todoNumber <= 20 ? digitWithPeriod[todoNumber - 1] : `${todoNumber}.`
|
|
605
800
|
const content =
|
|
606
801
|
activeTodo.content.charAt(0).toLowerCase() + activeTodo.content.slice(1)
|
|
607
|
-
return `${
|
|
802
|
+
return `${todoNumber}. **${escapeInlineMarkdown(content)}**`
|
|
608
803
|
}
|
|
609
804
|
|
|
610
805
|
export function formatTaskToolTitle(part: Extract<Part, { type: 'tool' }>): string {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const childSessionId = part.state.metadata?.sessionId
|
|
614
|
-
if (typeof childSessionId !== 'string' || !childSessionId) return ''
|
|
806
|
+
// Running only. The child session can be created later when many tasks queue.
|
|
807
|
+
if (part.tool !== 'task' || part.state.status !== 'running') return ''
|
|
615
808
|
|
|
616
809
|
const description = part.state.input?.description
|
|
617
|
-
const stateTitle = part.state.
|
|
618
|
-
? part.state.title
|
|
619
|
-
: undefined
|
|
810
|
+
const stateTitle = part.state.title
|
|
620
811
|
const title = typeof description === 'string' && description
|
|
621
812
|
? description
|
|
622
813
|
: typeof stateTitle === 'string'
|
|
@@ -626,7 +817,7 @@ export function formatTaskToolTitle(part: Extract<Part, { type: 'tool' }>): stri
|
|
|
626
817
|
|
|
627
818
|
const subagentType = part.state.input?.subagent_type
|
|
628
819
|
const agent = typeof subagentType === 'string' ? subagentType : 'task'
|
|
629
|
-
return
|
|
820
|
+
return `${TOOL_PREFIX}${escapeInlineMarkdown(agent)} **${escapeInlineMarkdown(title)}**`
|
|
630
821
|
}
|
|
631
822
|
|
|
632
823
|
export function formatPart(part: Part, prefix?: string): string {
|
|
@@ -635,25 +826,15 @@ export function formatPart(part: Part, prefix?: string): string {
|
|
|
635
826
|
if (part.type === 'text') {
|
|
636
827
|
const text = part.text?.trim()
|
|
637
828
|
if (!text) return ''
|
|
638
|
-
// For subtask text, always use bullet with prefix
|
|
639
829
|
if (prefix) {
|
|
640
|
-
return
|
|
830
|
+
return `${pfx}${text}`
|
|
641
831
|
}
|
|
642
|
-
|
|
643
|
-
const markdownStarters = ['#', '*', '_', '-', '>', '`', '[', '|']
|
|
644
|
-
const startsWithMarkdown =
|
|
645
|
-
markdownStarters.includes(firstChar) ||
|
|
646
|
-
/^\d+\./.test(text) ||
|
|
647
|
-
/^<callout[\s>]/i.test(text)
|
|
648
|
-
if (startsWithMarkdown) {
|
|
649
|
-
return `\n${text}`
|
|
650
|
-
}
|
|
651
|
-
return `⬥ ${text}`
|
|
832
|
+
return text
|
|
652
833
|
}
|
|
653
834
|
|
|
654
835
|
if (part.type === 'reasoning') {
|
|
655
836
|
if (!part.text?.trim()) return ''
|
|
656
|
-
return
|
|
837
|
+
return `${THINKING_PREFIX}${pfx}thinking`
|
|
657
838
|
}
|
|
658
839
|
|
|
659
840
|
if (part.type === 'file') {
|
|
@@ -665,23 +846,24 @@ export function formatPart(part: Part, prefix?: string): string {
|
|
|
665
846
|
if (
|
|
666
847
|
part.type === 'step-start' ||
|
|
667
848
|
part.type === 'step-finish' ||
|
|
668
|
-
part.type === 'patch'
|
|
849
|
+
part.type === 'patch' ||
|
|
850
|
+
part.type === 'compaction'
|
|
669
851
|
) {
|
|
670
852
|
return ''
|
|
671
853
|
}
|
|
672
854
|
|
|
673
855
|
if (part.type === 'agent') {
|
|
674
|
-
return
|
|
856
|
+
return `${TOOL_PREFIX}${pfx}agent ${part.id}`
|
|
675
857
|
}
|
|
676
858
|
|
|
677
859
|
if (part.type === 'snapshot') {
|
|
678
|
-
return
|
|
860
|
+
return `${TOOL_PREFIX}${pfx}snapshot ${part.snapshot}`
|
|
679
861
|
}
|
|
680
862
|
|
|
681
863
|
if (part.type === 'tool') {
|
|
682
864
|
if (part.tool === 'todowrite') {
|
|
683
865
|
const formatted = formatTodoList(part)
|
|
684
|
-
return prefix && formatted ?
|
|
866
|
+
return prefix && formatted ? `${TOOL_PREFIX}${pfx}${formatted}` : formatted
|
|
685
867
|
}
|
|
686
868
|
|
|
687
869
|
// Question tool is handled via Discord dropdowns, not text
|
|
@@ -710,11 +892,13 @@ export function formatPart(part: Part, prefix?: string): string {
|
|
|
710
892
|
}
|
|
711
893
|
const command = (part.state.input?.command as string) || ''
|
|
712
894
|
const description = (part.state.input?.description as string) || ''
|
|
895
|
+
const summary = (part.state.input?.summary as string) || ''
|
|
713
896
|
const toolTitle = formatBashToolTitle({
|
|
714
897
|
command,
|
|
715
898
|
description,
|
|
899
|
+
summary,
|
|
716
900
|
})
|
|
717
|
-
return
|
|
901
|
+
return `${TOOL_PREFIX}${pfx}bash${toolTitle}`
|
|
718
902
|
}
|
|
719
903
|
|
|
720
904
|
const summaryText = getToolSummaryText(part)
|
|
@@ -726,9 +910,11 @@ export function formatPart(part: Part, prefix?: string): string {
|
|
|
726
910
|
} else if (part.tool === 'bash') {
|
|
727
911
|
const command = (part.state.input?.command as string) || ''
|
|
728
912
|
const description = (part.state.input?.description as string) || ''
|
|
913
|
+
const summary = (part.state.input?.summary as string) || ''
|
|
729
914
|
const formatted = formatBashToolTitle({
|
|
730
915
|
command,
|
|
731
916
|
description,
|
|
917
|
+
summary,
|
|
732
918
|
stateTitle,
|
|
733
919
|
})
|
|
734
920
|
toolTitle = formatted.startsWith(' ') ? formatted.slice(1) : formatted
|
|
@@ -745,14 +931,17 @@ export function formatPart(part: Part, prefix?: string): string {
|
|
|
745
931
|
part.tool === 'write' ||
|
|
746
932
|
part.tool === 'apply_patch'
|
|
747
933
|
) {
|
|
748
|
-
return
|
|
934
|
+
return FILE_EDIT_PREFIX
|
|
749
935
|
}
|
|
750
|
-
return
|
|
936
|
+
return TOOL_PREFIX
|
|
751
937
|
})()
|
|
752
938
|
const toolParts = [part.tool, toolTitle, summaryText]
|
|
753
939
|
.filter(Boolean)
|
|
754
940
|
.join(' ')
|
|
755
|
-
|
|
941
|
+
if (icon === '⨯') {
|
|
942
|
+
return `${icon} ${pfx}${toolParts}`
|
|
943
|
+
}
|
|
944
|
+
return `${icon}${pfx}${toolParts}`
|
|
756
945
|
}
|
|
757
946
|
|
|
758
947
|
logger.warn('Unknown part type:', part)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Message pre-processing pipeline for incoming Discord messages.
|
|
2
2
|
// Extracts prompt text, voice transcription, file/text attachments, and
|
|
3
3
|
// session context from a Discord Message before handing off to the runtime.
|
|
4
|
+
// Voice side/fresh-chat requests dispatch to separate threads instead.
|
|
4
5
|
//
|
|
5
6
|
// This module exists so discord-bot.ts stays a thin event router and the
|
|
6
7
|
// expensive async work (voice transcription, context fetch, attachment
|
|
@@ -11,6 +12,7 @@ import type { Message, ThreadChannel } from 'discord.js'
|
|
|
11
12
|
import { DiscordOperationError, OpenCodeSdkError } from './errors.js'
|
|
12
13
|
import type { DiscordFileAttachment } from './message-formatting.js'
|
|
13
14
|
import type { PreprocessResult } from './session-handler/thread-session-runtime.js'
|
|
15
|
+
import { getOrCreateRuntime } from './session-handler/thread-session-runtime.js'
|
|
14
16
|
import type { AgentInfo, RepliedMessageContext } from './system-message.js'
|
|
15
17
|
import {
|
|
16
18
|
resolveMentions,
|
|
@@ -21,7 +23,11 @@ import { processVoiceAttachment } from './voice-handler.js'
|
|
|
21
23
|
import { isVoiceAttachment } from './voice-attachment.js'
|
|
22
24
|
import { initializeOpencodeForDirectory } from './opencode.js'
|
|
23
25
|
import { getCompactSessionContext, getLastSessionId } from './markdown.js'
|
|
24
|
-
import { getThreadSession } from './database.js'
|
|
26
|
+
import { getThreadSession, getThreadWorktreeOrWorkspace } from './database.js'
|
|
27
|
+
import { resolveWorkingDirectory, resolveTextChannel, sendThreadMessage } from './discord-utils.js'
|
|
28
|
+
import { forkSessionToBtwThread } from './commands/btw.js'
|
|
29
|
+
import { createNewSessionThread } from './commands/session.js'
|
|
30
|
+
import type { TranscriptionResult } from './voice.js'
|
|
25
31
|
import * as errore from 'errore'
|
|
26
32
|
import { createLogger, LogPrefix } from './logger.js'
|
|
27
33
|
import { notifyError } from './sentry.js'
|
|
@@ -32,6 +38,96 @@ const voiceLogger = createLogger(LogPrefix.VOICE)
|
|
|
32
38
|
export const VOICE_MESSAGE_TRANSCRIPTION_PREFIX =
|
|
33
39
|
'Voice message transcription from Discord user:\n'
|
|
34
40
|
|
|
41
|
+
// Dispatch before returning a prompt so the source runtime never receives the side request.
|
|
42
|
+
async function routeVoiceSession({
|
|
43
|
+
voiceResult,
|
|
44
|
+
message,
|
|
45
|
+
thread,
|
|
46
|
+
appId,
|
|
47
|
+
}: {
|
|
48
|
+
voiceResult: TranscriptionResult | null
|
|
49
|
+
message: Message
|
|
50
|
+
thread: ThreadChannel
|
|
51
|
+
appId?: string
|
|
52
|
+
}): Promise<boolean> {
|
|
53
|
+
if (!voiceResult?.sessionAction) return false
|
|
54
|
+
const resolved = await resolveWorkingDirectory({ channel: thread })
|
|
55
|
+
if (!resolved) {
|
|
56
|
+
await sendThreadMessage(thread, 'Could not determine project directory. Use /add-project to configure it, then retry.')
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
const images = await getFileAttachments(message)
|
|
60
|
+
const textAttachments = await getTextAttachments(message)
|
|
61
|
+
const caption = resolveMentions(message).trim()
|
|
62
|
+
const prompt = [caption, voiceResult.transcription, textAttachments]
|
|
63
|
+
.filter((part) => part?.trim())
|
|
64
|
+
.join('\n\n')
|
|
65
|
+
const modelPrompt = [caption, `${VOICE_MESSAGE_TRANSCRIPTION_PREFIX}${voiceResult.transcription}`, textAttachments]
|
|
66
|
+
.filter((part) => part?.trim())
|
|
67
|
+
.join('\n\n')
|
|
68
|
+
if (voiceResult.sessionAction === 'btw') {
|
|
69
|
+
const result = await forkSessionToBtwThread({
|
|
70
|
+
sourceThread: thread,
|
|
71
|
+
projectDirectory: resolved.projectDirectory,
|
|
72
|
+
sdkDirectory: resolved.workingDirectory,
|
|
73
|
+
prompt,
|
|
74
|
+
modelPrompt,
|
|
75
|
+
images,
|
|
76
|
+
agent: voiceResult.agent,
|
|
77
|
+
userId: message.author.id,
|
|
78
|
+
username: message.member?.displayName || message.author.displayName,
|
|
79
|
+
appId,
|
|
80
|
+
})
|
|
81
|
+
.catch((cause) => new DiscordOperationError({ operation: 'forkVoiceSession', cause }))
|
|
82
|
+
if (result instanceof Error) {
|
|
83
|
+
logger.error('Voice side chat failed:', result)
|
|
84
|
+
await sendThreadMessage(thread, `${result.message}. Start a session first, then retry with /btw.`)
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
await sendThreadMessage(thread, `Session forked! Continue in ${result.thread.toString()}`)
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
const textChannel = await resolveTextChannel(thread)
|
|
91
|
+
if (!textChannel) {
|
|
92
|
+
await sendThreadMessage(thread, 'Could not resolve parent text channel. Retry /new-session in the project channel.')
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
const result = await createNewSessionThread({
|
|
96
|
+
textChannel,
|
|
97
|
+
projectDirectory: resolved.projectDirectory,
|
|
98
|
+
prompt,
|
|
99
|
+
userId: message.author.id,
|
|
100
|
+
sourceWorkspace: await getThreadWorktreeOrWorkspace(thread.id),
|
|
101
|
+
}).catch((cause) => new DiscordOperationError({ operation: 'createVoiceSessionThread', cause }))
|
|
102
|
+
if (result instanceof Error) {
|
|
103
|
+
logger.error('Voice new session failed:', result)
|
|
104
|
+
await sendThreadMessage(thread, `${result.message}. Retry with /new-session.`)
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
await sendThreadMessage(thread, `Created new session in ${result.toString()}`)
|
|
108
|
+
const runtime = getOrCreateRuntime({
|
|
109
|
+
threadId: result.id,
|
|
110
|
+
thread: result,
|
|
111
|
+
projectDirectory: resolved.projectDirectory,
|
|
112
|
+
sdkDirectory: resolved.workingDirectory,
|
|
113
|
+
channelId: textChannel.id,
|
|
114
|
+
appId,
|
|
115
|
+
})
|
|
116
|
+
await runtime.enqueueIncoming({
|
|
117
|
+
prompt: modelPrompt,
|
|
118
|
+
images,
|
|
119
|
+
agent: voiceResult.agent,
|
|
120
|
+
userId: message.author.id,
|
|
121
|
+
username: message.member?.displayName || message.author.displayName,
|
|
122
|
+
appId,
|
|
123
|
+
mode: 'opencode',
|
|
124
|
+
}).catch(async (error) => {
|
|
125
|
+
logger.error('Voice session dispatch failed:', error)
|
|
126
|
+
await sendThreadMessage(result, 'Could not send the request to OpenCode. Send your request again in this thread.')
|
|
127
|
+
})
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
|
|
35
131
|
/** Fetch available agents from OpenCode for voice transcription agent selection. */
|
|
36
132
|
async function fetchAvailableAgents(
|
|
37
133
|
getClient: Awaited<ReturnType<typeof initializeOpencodeForDirectory>>,
|
|
@@ -245,7 +341,15 @@ export async function preprocessExistingThreadMessage({
|
|
|
245
341
|
currentSessionContext,
|
|
246
342
|
lastSessionContext,
|
|
247
343
|
agents,
|
|
344
|
+
canForkSession: true,
|
|
248
345
|
})
|
|
346
|
+
if (voiceResult instanceof Error) {
|
|
347
|
+
voiceLogger.warn('Voice request not dispatched:', voiceResult)
|
|
348
|
+
return { prompt: '', mode: 'opencode', skip: true }
|
|
349
|
+
}
|
|
350
|
+
if (await routeVoiceSession({ voiceResult, message, thread, appId })) {
|
|
351
|
+
return { prompt: '', mode: 'opencode', skip: true }
|
|
352
|
+
}
|
|
249
353
|
if (voiceResult) {
|
|
250
354
|
messageContent = `${VOICE_MESSAGE_TRANSCRIPTION_PREFIX}${voiceResult.transcription}`
|
|
251
355
|
}
|
|
@@ -327,6 +431,13 @@ export async function preprocessNewSessionMessage({
|
|
|
327
431
|
appId,
|
|
328
432
|
agents,
|
|
329
433
|
})
|
|
434
|
+
if (voiceResult instanceof Error) {
|
|
435
|
+
voiceLogger.warn('Voice request not dispatched:', voiceResult)
|
|
436
|
+
return { prompt: '', mode: 'opencode', skip: true }
|
|
437
|
+
}
|
|
438
|
+
if (await routeVoiceSession({ voiceResult, message, thread, appId })) {
|
|
439
|
+
return { prompt: '', mode: 'opencode', skip: true }
|
|
440
|
+
}
|
|
330
441
|
if (voiceResult) {
|
|
331
442
|
prompt = `${VOICE_MESSAGE_TRANSCRIPTION_PREFIX}${voiceResult.transcription}`
|
|
332
443
|
}
|
|
@@ -424,6 +535,10 @@ export async function preprocessNewThreadMessage({
|
|
|
424
535
|
appId,
|
|
425
536
|
agents,
|
|
426
537
|
})
|
|
538
|
+
if (voiceResult instanceof Error) {
|
|
539
|
+
voiceLogger.warn('Voice request not dispatched:', voiceResult)
|
|
540
|
+
return { prompt: '', mode: 'opencode', skip: true }
|
|
541
|
+
}
|
|
427
542
|
if (voiceResult) {
|
|
428
543
|
messageContent = `${VOICE_MESSAGE_TRANSCRIPTION_PREFIX}${voiceResult.transcription}`
|
|
429
544
|
}
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LEGACY. Superseded by @subrouter/opencode, registered alongside the kimaki
|
|
3
|
+
* plugin in opencode.ts. This whole rotation path only cycles accounts inside
|
|
4
|
+
* one provider; subrouter also fails over to a different provider when every
|
|
5
|
+
* account of one is exhausted.
|
|
6
|
+
*
|
|
7
|
+
* Kept because opencode ships no Claude Pro/Max auth of its own, so deleting
|
|
8
|
+
* it would strand anyone still picking `anthropic/*` directly. To move to
|
|
9
|
+
* subrouter, log in again through /login and pick Subrouter.
|
|
10
|
+
*/
|
|
1
11
|
/**
|
|
2
12
|
* Shared utilities for multi-provider OAuth account rotation.
|
|
3
13
|
* Used by both anthropic-auth-state.ts and openai-auth-state.ts to avoid
|
package/src/openai-auth-state.ts
CHANGED