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
|
@@ -4,7 +4,7 @@ import fs from 'node:fs'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import type { CommandContext, AutocompleteContext } from './types.js'
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
findRegisteredTextChannelForDirectory,
|
|
8
8
|
getAllTextChannelDirectories,
|
|
9
9
|
} from '../database.js'
|
|
10
10
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
@@ -56,14 +56,10 @@ export async function handleAddProjectCommand({
|
|
|
56
56
|
return
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
channelType: 'text',
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
if (existingChannels.length > 0) {
|
|
59
|
+
const existingChannel = await findRegisteredTextChannelForDirectory(directory)
|
|
60
|
+
if (existingChannel) {
|
|
65
61
|
await command.editReply(
|
|
66
|
-
`A channel already exists for this directory: <#${
|
|
62
|
+
`A channel already exists for this directory: <#${existingChannel.channel_id}>`,
|
|
67
63
|
)
|
|
68
64
|
return
|
|
69
65
|
}
|
package/src/commands/agent.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Also provides quick agent commands like /plan-agent, /build-agent that switch instantly.
|
|
3
3
|
// When a prompt is provided to a quick agent command (e.g. /plan-agent "fix the bug"),
|
|
4
4
|
// the prompt is sent with that agent and the session keeps that agent afterwards.
|
|
5
|
+
// Optional last `variant` option sets the model thinking level for that agent.
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
8
|
ChatInputCommandInteraction,
|
|
@@ -18,6 +19,9 @@ import crypto from 'node:crypto'
|
|
|
18
19
|
import {
|
|
19
20
|
setChannelAgent,
|
|
20
21
|
setSessionAgent,
|
|
22
|
+
setSessionModel,
|
|
23
|
+
setChannelModel,
|
|
24
|
+
getChannelModel,
|
|
21
25
|
clearSessionModel,
|
|
22
26
|
getThreadSession,
|
|
23
27
|
getSessionAgent,
|
|
@@ -28,10 +32,15 @@ import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
|
28
32
|
import {
|
|
29
33
|
resolveTextChannel,
|
|
30
34
|
resolveWorkingDirectory,
|
|
35
|
+
resolveProjectDirectoryFromAutocomplete,
|
|
31
36
|
getKimakiMetadata,
|
|
32
37
|
SILENT_MESSAGE_FLAGS,
|
|
33
38
|
} from '../discord-utils.js'
|
|
34
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
getOrCreateRuntime,
|
|
41
|
+
releaseCurrentThreadIngress,
|
|
42
|
+
waitForCurrentThreadIngress,
|
|
43
|
+
} from '../session-handler/thread-session-runtime.js'
|
|
35
44
|
import { createLogger, LogPrefix } from '../logger.js'
|
|
36
45
|
import {
|
|
37
46
|
formatModelSource,
|
|
@@ -45,7 +54,14 @@ import {
|
|
|
45
54
|
worktreeCreatingMessage,
|
|
46
55
|
} from './new-worktree.js'
|
|
47
56
|
import { WORKTREE_PREFIX } from './merge-worktree.js'
|
|
57
|
+
import { QUEUE_PREFIX } from '../message-formatting.js'
|
|
48
58
|
import { store } from '../store.js'
|
|
59
|
+
import {
|
|
60
|
+
getThinkingValuesForModel,
|
|
61
|
+
resolveRequestedThinkingVariant,
|
|
62
|
+
} from '../thinking-utils.js'
|
|
63
|
+
import type { AutocompleteContext } from './types.js'
|
|
64
|
+
import { OpenCodeSdkError } from '../errors.js'
|
|
49
65
|
|
|
50
66
|
const agentLogger = createLogger(LogPrefix.AGENT)
|
|
51
67
|
|
|
@@ -269,6 +285,65 @@ export async function setAgentForContext({
|
|
|
269
285
|
}
|
|
270
286
|
}
|
|
271
287
|
|
|
288
|
+
async function applyQuickAgentVariant({
|
|
289
|
+
context,
|
|
290
|
+
agentName,
|
|
291
|
+
variant,
|
|
292
|
+
}: {
|
|
293
|
+
context: AgentCommandContext
|
|
294
|
+
agentName: string
|
|
295
|
+
variant: string
|
|
296
|
+
}): Promise<string | undefined> {
|
|
297
|
+
const modelInfo = await resolveAgentModelInfo({ context, agentName })
|
|
298
|
+
if (modelInfo.type === 'none') {
|
|
299
|
+
agentLogger.warn(
|
|
300
|
+
`[AGENT] Skipping variant ${variant}: no model for agent ${agentName}`,
|
|
301
|
+
)
|
|
302
|
+
return undefined
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const getClient = await initializeOpencodeForDirectory(context.dir)
|
|
306
|
+
if (getClient instanceof Error) return undefined
|
|
307
|
+
|
|
308
|
+
const providersResponse = await getClient()
|
|
309
|
+
.provider.list({ directory: context.workingDirectory })
|
|
310
|
+
.catch((e) => new OpenCodeSdkError({ operation: 'provider.list', cause: e }))
|
|
311
|
+
if (providersResponse instanceof Error || !providersResponse.data) {
|
|
312
|
+
agentLogger.warn(`[AGENT] Skipping variant ${variant}: provider list failed`)
|
|
313
|
+
return undefined
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const matchedVariant = resolveRequestedThinkingVariant({
|
|
317
|
+
requestedValue: variant,
|
|
318
|
+
providers: providersResponse.data.all,
|
|
319
|
+
providerId: modelInfo.providerID,
|
|
320
|
+
modelId: modelInfo.modelID,
|
|
321
|
+
})
|
|
322
|
+
if (!matchedVariant) {
|
|
323
|
+
agentLogger.warn(
|
|
324
|
+
`[AGENT] Skipping variant ${variant}: not supported by ${modelInfo.model}`,
|
|
325
|
+
)
|
|
326
|
+
return undefined
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (context.isThread && context.sessionId) {
|
|
330
|
+
await setSessionModel({
|
|
331
|
+
sessionId: context.sessionId,
|
|
332
|
+
modelId: modelInfo.model,
|
|
333
|
+
variant: matchedVariant,
|
|
334
|
+
})
|
|
335
|
+
return matchedVariant
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const existing = await getChannelModel(context.channelId)
|
|
339
|
+
await setChannelModel({
|
|
340
|
+
channelId: context.channelId,
|
|
341
|
+
modelId: existing?.modelId ?? modelInfo.model,
|
|
342
|
+
variant: matchedVariant,
|
|
343
|
+
})
|
|
344
|
+
return matchedVariant
|
|
345
|
+
}
|
|
346
|
+
|
|
272
347
|
function formatAgentModelLine(modelInfo: CurrentModelInfo): string {
|
|
273
348
|
if (modelInfo.type === 'none') return ''
|
|
274
349
|
const source = formatModelSource({
|
|
@@ -287,21 +362,24 @@ function formatAgentPreferenceReply({
|
|
|
287
362
|
previousAgentName,
|
|
288
363
|
modelInfo,
|
|
289
364
|
scope,
|
|
365
|
+
variant,
|
|
290
366
|
}: {
|
|
291
367
|
agentName: string
|
|
292
368
|
previousAgentName?: string
|
|
293
369
|
modelInfo: CurrentModelInfo
|
|
294
370
|
scope: 'session' | 'channel'
|
|
371
|
+
variant?: string
|
|
295
372
|
}): string {
|
|
296
373
|
const sameAgent = previousAgentName === agentName
|
|
297
374
|
const verb = sameAgent ? 'Using' : 'Switched to'
|
|
298
375
|
const previousText =
|
|
299
376
|
!sameAgent && previousAgentName ? ` (was **${previousAgentName}**)` : ''
|
|
300
377
|
const modelText = formatAgentModelLine(modelInfo)
|
|
378
|
+
const variantText = variant ? `\nVariant: **${variant}**` : ''
|
|
301
379
|
if (scope === 'session') {
|
|
302
|
-
return `${verb} **${agentName}** agent for this session${previousText}${modelText}\nThe agent will change on the next message.`
|
|
380
|
+
return `${verb} **${agentName}** agent for this session${previousText}${modelText}${variantText}\nThe agent will change on the next message.`
|
|
303
381
|
}
|
|
304
|
-
return `${verb} **${agentName}** agent for this channel${previousText}${modelText}\nAll new sessions will use this agent.`
|
|
382
|
+
return `${verb} **${agentName}** agent for this channel${previousText}${modelText}${variantText}\nAll new sessions will use this agent.`
|
|
305
383
|
}
|
|
306
384
|
|
|
307
385
|
async function resolveAgentModelInfo({
|
|
@@ -452,7 +530,12 @@ export async function handleAgentSelectMenu(
|
|
|
452
530
|
const previousAgentName =
|
|
453
531
|
previousAgent.type !== 'none' ? previousAgent.agent : undefined
|
|
454
532
|
|
|
455
|
-
await
|
|
533
|
+
await waitForCurrentThreadIngress()
|
|
534
|
+
try {
|
|
535
|
+
await setAgentForContext({ context, agentName: selectedAgent })
|
|
536
|
+
} finally {
|
|
537
|
+
releaseCurrentThreadIngress()
|
|
538
|
+
}
|
|
456
539
|
const modelInfo = await resolveAgentModelInfo({
|
|
457
540
|
context,
|
|
458
541
|
agentName: selectedAgent,
|
|
@@ -498,10 +581,17 @@ export async function handleQuickAgentCommand({
|
|
|
498
581
|
}): Promise<void> {
|
|
499
582
|
const fallbackAgentName = command.commandName.replace(/-agent$/, '')
|
|
500
583
|
const prompt = command.options.getString('prompt') || undefined
|
|
584
|
+
const variant = command.options.getString('variant') || undefined
|
|
501
585
|
|
|
502
586
|
// Prompt mode: send the prompt with this agent immediately.
|
|
503
587
|
if (prompt) {
|
|
504
|
-
return handleQuickAgentWithPrompt({
|
|
588
|
+
return handleQuickAgentWithPrompt({
|
|
589
|
+
command,
|
|
590
|
+
appId,
|
|
591
|
+
fallbackAgentName,
|
|
592
|
+
prompt,
|
|
593
|
+
variant,
|
|
594
|
+
})
|
|
505
595
|
}
|
|
506
596
|
|
|
507
597
|
// No prompt: switch the persistent agent preference (original behavior).
|
|
@@ -529,23 +619,39 @@ export async function handleQuickAgentCommand({
|
|
|
529
619
|
const previousAgentName =
|
|
530
620
|
previousAgent.type !== 'none' ? previousAgent.agent : undefined
|
|
531
621
|
|
|
532
|
-
await
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
})
|
|
538
|
-
const scope =
|
|
539
|
-
context.isThread && context.sessionId ? 'session' : 'channel'
|
|
540
|
-
|
|
541
|
-
await command.editReply({
|
|
542
|
-
content: formatAgentPreferenceReply({
|
|
622
|
+
await waitForCurrentThreadIngress()
|
|
623
|
+
try {
|
|
624
|
+
await setAgentForContext({ context, agentName: resolvedAgentName })
|
|
625
|
+
const modelInfo = await resolveAgentModelInfo({
|
|
626
|
+
context,
|
|
543
627
|
agentName: resolvedAgentName,
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
628
|
+
})
|
|
629
|
+
const appliedVariant = variant
|
|
630
|
+
? await applyQuickAgentVariant({
|
|
631
|
+
context,
|
|
632
|
+
agentName: resolvedAgentName,
|
|
633
|
+
variant,
|
|
634
|
+
})
|
|
635
|
+
: undefined
|
|
636
|
+
const scope =
|
|
637
|
+
context.isThread && context.sessionId ? 'session' : 'channel'
|
|
638
|
+
const unsupportedVariantNote =
|
|
639
|
+
variant && !appliedVariant
|
|
640
|
+
? `\nVariant **${variant}** is not supported by this agent's model.`
|
|
641
|
+
: ''
|
|
642
|
+
|
|
643
|
+
await command.editReply({
|
|
644
|
+
content: `${formatAgentPreferenceReply({
|
|
645
|
+
agentName: resolvedAgentName,
|
|
646
|
+
previousAgentName,
|
|
647
|
+
modelInfo,
|
|
648
|
+
scope,
|
|
649
|
+
variant: appliedVariant,
|
|
650
|
+
})}${unsupportedVariantNote}`,
|
|
651
|
+
})
|
|
652
|
+
} finally {
|
|
653
|
+
releaseCurrentThreadIngress()
|
|
654
|
+
}
|
|
549
655
|
} catch (error) {
|
|
550
656
|
agentLogger.error('Error in quick agent command:', error)
|
|
551
657
|
await command.editReply({
|
|
@@ -565,11 +671,13 @@ async function handleQuickAgentWithPrompt({
|
|
|
565
671
|
appId,
|
|
566
672
|
fallbackAgentName,
|
|
567
673
|
prompt,
|
|
674
|
+
variant,
|
|
568
675
|
}: {
|
|
569
676
|
command: ChatInputCommandInteraction
|
|
570
677
|
appId: string
|
|
571
678
|
fallbackAgentName: string
|
|
572
679
|
prompt: string
|
|
680
|
+
variant?: string
|
|
573
681
|
}): Promise<void> {
|
|
574
682
|
const channel = command.channel
|
|
575
683
|
if (!channel) {
|
|
@@ -615,7 +723,7 @@ async function handleQuickAgentWithPrompt({
|
|
|
615
723
|
|
|
616
724
|
// Visible reply showing the one-shot prompt (not ephemeral, so it appears in thread).
|
|
617
725
|
await command.reply({
|
|
618
|
-
content:
|
|
726
|
+
content: `${QUEUE_PREFIX}**${command.user.displayName}** (${resolvedAgentName}): ${displayText}`,
|
|
619
727
|
flags: SILENT_MESSAGE_FLAGS,
|
|
620
728
|
})
|
|
621
729
|
|
|
@@ -626,6 +734,7 @@ async function handleQuickAgentWithPrompt({
|
|
|
626
734
|
agent: resolvedAgentName,
|
|
627
735
|
appId,
|
|
628
736
|
mode: 'opencode',
|
|
737
|
+
variant,
|
|
629
738
|
})
|
|
630
739
|
} else if (channel.type === ChannelType.GuildText) {
|
|
631
740
|
// In a channel: create a new thread and enqueue with the requested agent.
|
|
@@ -662,7 +771,7 @@ async function handleQuickAgentWithPrompt({
|
|
|
662
771
|
: baseThreadName
|
|
663
772
|
|
|
664
773
|
const starterMessage = await channel.send({
|
|
665
|
-
content:
|
|
774
|
+
content: `${QUEUE_PREFIX}**${command.user.displayName}** (${resolvedAgentName}): ${displayText}`,
|
|
666
775
|
flags: SILENT_MESSAGE_FLAGS,
|
|
667
776
|
})
|
|
668
777
|
|
|
@@ -696,12 +805,12 @@ async function handleQuickAgentWithPrompt({
|
|
|
696
805
|
})
|
|
697
806
|
}
|
|
698
807
|
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
808
|
+
const worktreeResult = worktreePromise ? await worktreePromise : projectDirectory
|
|
809
|
+
if (worktreeResult instanceof Error) {
|
|
810
|
+
await command.editReply(`Worktree creation failed: ${worktreeResult.message}`)
|
|
811
|
+
return
|
|
812
|
+
}
|
|
813
|
+
const sessionDirectory = worktreeResult
|
|
705
814
|
|
|
706
815
|
await command
|
|
707
816
|
.editReply(`Sent with **${resolvedAgentName}** agent in ${thread.toString()}`)
|
|
@@ -725,6 +834,7 @@ async function handleQuickAgentWithPrompt({
|
|
|
725
834
|
agent: resolvedAgentName,
|
|
726
835
|
appId,
|
|
727
836
|
mode: 'opencode',
|
|
837
|
+
variant,
|
|
728
838
|
})
|
|
729
839
|
} else {
|
|
730
840
|
await command.reply({
|
|
@@ -733,3 +843,69 @@ async function handleQuickAgentWithPrompt({
|
|
|
733
843
|
})
|
|
734
844
|
}
|
|
735
845
|
}
|
|
846
|
+
|
|
847
|
+
export async function handleQuickAgentAutocomplete({
|
|
848
|
+
interaction,
|
|
849
|
+
}: AutocompleteContext): Promise<void> {
|
|
850
|
+
if (interaction.options.getFocused(true).name !== 'variant') {
|
|
851
|
+
await interaction.respond([])
|
|
852
|
+
return
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const focusedValue = interaction.options.getFocused().trim().toLowerCase()
|
|
856
|
+
const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction)
|
|
857
|
+
if (!projectDirectory) {
|
|
858
|
+
await interaction.respond([])
|
|
859
|
+
return
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
863
|
+
if (getClient instanceof Error) {
|
|
864
|
+
await interaction.respond([])
|
|
865
|
+
return
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const [providersResponse, agentsResponse] = await Promise.all([
|
|
869
|
+
getClient()
|
|
870
|
+
.provider.list({ directory: projectDirectory })
|
|
871
|
+
.catch((e) => new OpenCodeSdkError({ operation: 'provider.list', cause: e })),
|
|
872
|
+
getClient()
|
|
873
|
+
.app.agents({ directory: projectDirectory })
|
|
874
|
+
.catch((e) => new OpenCodeSdkError({ operation: 'app.agents', cause: e })),
|
|
875
|
+
])
|
|
876
|
+
if (providersResponse instanceof Error || agentsResponse instanceof Error) {
|
|
877
|
+
agentLogger.warn('[AUTOCOMPLETE] Failed to fetch variant choices')
|
|
878
|
+
await interaction.respond([])
|
|
879
|
+
return
|
|
880
|
+
}
|
|
881
|
+
if (!providersResponse.data || !agentsResponse.data) {
|
|
882
|
+
await interaction.respond([])
|
|
883
|
+
return
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const resolvedAgentName =
|
|
887
|
+
parseQuickAgentNameFromDescription(interaction.command?.description) ||
|
|
888
|
+
interaction.commandName.replace(/-agent$/, '')
|
|
889
|
+
const agent = agentsResponse.data.find((candidate) => candidate.name === resolvedAgentName)
|
|
890
|
+
const agentModel = agent?.model
|
|
891
|
+
if (!agentModel) {
|
|
892
|
+
await interaction.respond([])
|
|
893
|
+
return
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const variants = getThinkingValuesForModel({
|
|
897
|
+
providers: providersResponse.data.all,
|
|
898
|
+
providerId: agentModel.providerID,
|
|
899
|
+
modelId: agentModel.modelID,
|
|
900
|
+
}).filter((variant) => {
|
|
901
|
+
if (!focusedValue) return true
|
|
902
|
+
return variant.toLowerCase().includes(focusedValue)
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
await interaction.respond(
|
|
906
|
+
variants.slice(0, 25).map((variant) => ({
|
|
907
|
+
name: variant.slice(0, 100),
|
|
908
|
+
value: variant,
|
|
909
|
+
})),
|
|
910
|
+
)
|
|
911
|
+
}
|
|
@@ -9,8 +9,11 @@ import {
|
|
|
9
9
|
showAskUserQuestionDropdowns,
|
|
10
10
|
} from './ask-question.js'
|
|
11
11
|
|
|
12
|
-
function createFakeThread(): ThreadChannel {
|
|
12
|
+
function createFakeThread({ failSend }: { failSend?: boolean } = {}): ThreadChannel {
|
|
13
13
|
const send = vi.fn(async () => {
|
|
14
|
+
if (failSend) {
|
|
15
|
+
throw new Error('Missing Permissions')
|
|
16
|
+
}
|
|
14
17
|
return { id: 'msg-1' }
|
|
15
18
|
})
|
|
16
19
|
|
|
@@ -109,6 +112,50 @@ describe('ask-question', () => {
|
|
|
109
112
|
expect([...pendingQuestionContexts.keys()]).toEqual(['ctx-3'])
|
|
110
113
|
})
|
|
111
114
|
|
|
115
|
+
test('does not schedule a question expiry timer', async () => {
|
|
116
|
+
const timeoutSpy = vi.spyOn(globalThis, 'setTimeout')
|
|
117
|
+
|
|
118
|
+
await showAskUserQuestionDropdowns({
|
|
119
|
+
thread: createFakeThread(),
|
|
120
|
+
sessionId: 'ses-1',
|
|
121
|
+
directory: '/project',
|
|
122
|
+
requestId: 'req-no-ttl',
|
|
123
|
+
input: {
|
|
124
|
+
questions: [{
|
|
125
|
+
question: 'Choose one',
|
|
126
|
+
header: 'Pick',
|
|
127
|
+
options: [
|
|
128
|
+
{ label: 'Alpha', description: 'A' },
|
|
129
|
+
{ label: 'Beta', description: 'B' },
|
|
130
|
+
],
|
|
131
|
+
}],
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
expect(timeoutSpy).not.toHaveBeenCalled()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('drops the context when the dropdown message fails to send', async () => {
|
|
139
|
+
await showAskUserQuestionDropdowns({
|
|
140
|
+
thread: createFakeThread({ failSend: true }),
|
|
141
|
+
sessionId: 'ses-1',
|
|
142
|
+
directory: '/project',
|
|
143
|
+
requestId: 'req-send-fails',
|
|
144
|
+
input: {
|
|
145
|
+
questions: [{
|
|
146
|
+
question: 'Choose one',
|
|
147
|
+
header: 'Pick',
|
|
148
|
+
options: [
|
|
149
|
+
{ label: 'Alpha', description: 'A' },
|
|
150
|
+
{ label: 'Beta', description: 'B' },
|
|
151
|
+
],
|
|
152
|
+
}],
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
expect(pendingQuestionContexts.size).toBe(0)
|
|
157
|
+
})
|
|
158
|
+
|
|
112
159
|
test('requires every question to have an answer', () => {
|
|
113
160
|
expect(areAllQuestionsAnswered({
|
|
114
161
|
totalQuestions: 3,
|
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
import crypto from 'node:crypto'
|
|
13
13
|
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
14
14
|
import { getOpencodeClient } from '../opencode.js'
|
|
15
|
+
import { DiscordOperationError } from '../errors.js'
|
|
15
16
|
import { createLogger, LogPrefix } from '../logger.js'
|
|
17
|
+
import { QUEUE_PREFIX } from '../message-formatting.js'
|
|
16
18
|
|
|
17
19
|
const logger = createLogger(LogPrefix.ASK_QUESTION)
|
|
18
20
|
|
|
@@ -44,8 +46,9 @@ type PendingQuestionContext = {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
// Store pending question contexts by hash.
|
|
47
|
-
// TTL
|
|
48
|
-
|
|
49
|
+
// No TTL on purpose: a question can only be answered by the user, so expiring it
|
|
50
|
+
// could only abort the run. Cleanup happens on answer, on a new user message, on
|
|
51
|
+
// abort (abortActiveRunInternal), and on thread dispose.
|
|
49
52
|
export const pendingQuestionContexts = new Map<string, PendingQuestionContext>()
|
|
50
53
|
|
|
51
54
|
export function areAllQuestionsAnswered({
|
|
@@ -157,32 +160,6 @@ export async function showAskUserQuestionDropdowns({
|
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
pendingQuestionContexts.set(contextHash, context)
|
|
160
|
-
// On TTL expiry: hide the dropdown UI and abort the session so OpenCode
|
|
161
|
-
// unblocks. We intentionally do NOT call question.reply() — sending 'Other'
|
|
162
|
-
// made the model think the user chose an option when they didn't.
|
|
163
|
-
setTimeout(async () => {
|
|
164
|
-
const ctx = pendingQuestionContexts.get(contextHash)
|
|
165
|
-
if (!ctx) {
|
|
166
|
-
return
|
|
167
|
-
}
|
|
168
|
-
// Delete context first so the dropdown becomes inert immediately.
|
|
169
|
-
// Without this, a user clicking during the abort() await would still
|
|
170
|
-
// be accepted by handleAskQuestionSelectMenu, then abort() would
|
|
171
|
-
// kill that valid run.
|
|
172
|
-
deletePendingQuestionContextsForRequest({
|
|
173
|
-
threadId: ctx.thread.id,
|
|
174
|
-
requestId: ctx.requestId,
|
|
175
|
-
})
|
|
176
|
-
// Abort the session so OpenCode isn't stuck waiting for a reply
|
|
177
|
-
const client = getOpencodeClient(ctx.directory)
|
|
178
|
-
if (client) {
|
|
179
|
-
await client.session.abort({
|
|
180
|
-
sessionID: ctx.sessionId,
|
|
181
|
-
}).catch((error) => {
|
|
182
|
-
logger.error('Failed to abort session after question expiry:', error)
|
|
183
|
-
})
|
|
184
|
-
}
|
|
185
|
-
}, QUESTION_CONTEXT_TTL_MS).unref()
|
|
186
163
|
|
|
187
164
|
// Send one message per question with its dropdown directly underneath
|
|
188
165
|
for (let i = 0; i < input.questions.length; i++) {
|
|
@@ -219,11 +196,24 @@ export async function showAskUserQuestionDropdowns({
|
|
|
219
196
|
const actionRow =
|
|
220
197
|
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
|
|
221
198
|
|
|
222
|
-
await thread.send({
|
|
199
|
+
const sendResult = await thread.send({
|
|
223
200
|
content: `**${(q.header || '').slice(0, 200)}**\n${q.question.slice(0, 1700)}`,
|
|
224
201
|
components: [actionRow],
|
|
225
202
|
flags: silent ? SILENT_MESSAGE_FLAGS : NOTIFY_MESSAGE_FLAGS,
|
|
226
|
-
})
|
|
203
|
+
}).catch((e) => new DiscordOperationError({ operation: 'sendQuestionDropdown', cause: e }))
|
|
204
|
+
// Without a visible dropdown the user can never answer, and there is no TTL
|
|
205
|
+
// to rescue the run, so drop the context and abort so OpenCode unblocks.
|
|
206
|
+
if (sendResult instanceof Error) {
|
|
207
|
+
logger.error('Failed to send question dropdown:', sendResult)
|
|
208
|
+
deletePendingQuestionContextsForRequest({ threadId: thread.id, requestId })
|
|
209
|
+
const client = getOpencodeClient(directory)
|
|
210
|
+
if (client) {
|
|
211
|
+
await client.session.abort({ sessionID: sessionId }).catch((error) => {
|
|
212
|
+
logger.error('Failed to abort session after question send failure:', error)
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
return
|
|
216
|
+
}
|
|
227
217
|
}
|
|
228
218
|
|
|
229
219
|
logger.log(
|
|
@@ -305,7 +295,7 @@ export async function handleAskQuestionSelectMenu(
|
|
|
305
295
|
const username = interaction.user.globalName || interaction.user.username
|
|
306
296
|
await sendThreadMessage(
|
|
307
297
|
context.thread,
|
|
308
|
-
|
|
298
|
+
`${QUEUE_PREFIX}**${username}:** ${answeredText}`,
|
|
309
299
|
)
|
|
310
300
|
|
|
311
301
|
// Check if all questions are answered
|
|
@@ -414,7 +404,7 @@ export function parseAskUserQuestionTool(part: {
|
|
|
414
404
|
* the tool answer so the model sees the user's response. The caller should
|
|
415
405
|
* NOT also enqueue the message as a new prompt.
|
|
416
406
|
* Returns 'replied' on success, 'reply-failed' if the reply call fails
|
|
417
|
-
* (context kept pending so
|
|
407
|
+
* (context kept pending so the user can retry).
|
|
418
408
|
*/
|
|
419
409
|
export async function cancelPendingQuestion(
|
|
420
410
|
threadId: string,
|
|
@@ -466,7 +456,7 @@ export async function cancelPendingQuestion(
|
|
|
466
456
|
logger.log(`Answered question ${context.requestId} with user message`)
|
|
467
457
|
} catch (error) {
|
|
468
458
|
logger.error('Failed to answer question:', error)
|
|
469
|
-
// Keep context pending so
|
|
459
|
+
// Keep context pending so the user can retry.
|
|
470
460
|
// Caller should not consume the user message since reply failed.
|
|
471
461
|
return 'reply-failed'
|
|
472
462
|
}
|
package/src/commands/btw.ts
CHANGED
|
@@ -9,7 +9,13 @@ import {
|
|
|
9
9
|
type ThreadChannel,
|
|
10
10
|
MessageFlags,
|
|
11
11
|
} from 'discord.js'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getThreadSession,
|
|
14
|
+
setThreadSession,
|
|
15
|
+
getThreadWorktreeOrWorkspace,
|
|
16
|
+
createPendingWorkspace,
|
|
17
|
+
setWorkspaceReady,
|
|
18
|
+
} from '../database.js'
|
|
13
19
|
import {
|
|
14
20
|
resolveWorkingDirectory,
|
|
15
21
|
resolveTextChannel,
|
|
@@ -20,6 +26,7 @@ import { createLogger, LogPrefix } from '../logger.js'
|
|
|
20
26
|
import type { CommandContext } from './types.js'
|
|
21
27
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
22
28
|
import { copyCurrentSessionModel } from './model.js'
|
|
29
|
+
import type { DiscordFileAttachment } from '../message-formatting.js'
|
|
23
30
|
|
|
24
31
|
const logger = createLogger(LogPrefix.FORK)
|
|
25
32
|
|
|
@@ -28,18 +35,24 @@ export async function forkSessionToBtwThread({
|
|
|
28
35
|
projectDirectory,
|
|
29
36
|
sdkDirectory,
|
|
30
37
|
prompt,
|
|
38
|
+
modelPrompt = prompt,
|
|
31
39
|
userId,
|
|
32
40
|
username,
|
|
33
41
|
appId,
|
|
42
|
+
agent,
|
|
43
|
+
images,
|
|
34
44
|
}: {
|
|
35
45
|
sourceThread: ThreadChannel
|
|
36
46
|
projectDirectory: string
|
|
37
47
|
/** Worktree directory when forking from a worktree thread, otherwise same as projectDirectory */
|
|
38
48
|
sdkDirectory: string
|
|
39
49
|
prompt: string
|
|
50
|
+
modelPrompt?: string
|
|
40
51
|
userId: string
|
|
41
52
|
username: string
|
|
42
53
|
appId: string | undefined
|
|
54
|
+
agent?: string
|
|
55
|
+
images?: DiscordFileAttachment[]
|
|
43
56
|
}): Promise<{ thread: ThreadChannel; forkedSessionId: string } | Error> {
|
|
44
57
|
// Parallelize: session lookup + opencode init + parent channel resolve are independent
|
|
45
58
|
const [sessionId, getClientResult, textChannel] = await Promise.all([
|
|
@@ -61,7 +74,7 @@ export async function forkSessionToBtwThread({
|
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
// Fork must succeed before creating the Discord thread to avoid orphan threads
|
|
64
|
-
const forkResponse = await getClientResult().session.fork({ sessionID: sessionId })
|
|
77
|
+
const forkResponse = await getClientResult().session.fork({ sessionID: sessionId, directory: sdkDirectory })
|
|
65
78
|
if (!forkResponse.data) {
|
|
66
79
|
return new Error('Failed to fork session')
|
|
67
80
|
}
|
|
@@ -74,7 +87,7 @@ export async function forkSessionToBtwThread({
|
|
|
74
87
|
channelId,
|
|
75
88
|
appId,
|
|
76
89
|
getClient: getClientResult,
|
|
77
|
-
directory:
|
|
90
|
+
directory: sdkDirectory,
|
|
78
91
|
})
|
|
79
92
|
|
|
80
93
|
const thread = await textChannel.threads.create({
|
|
@@ -85,11 +98,27 @@ export async function forkSessionToBtwThread({
|
|
|
85
98
|
|
|
86
99
|
// DB mapping must complete before user-visible actions so the thread is routable
|
|
87
100
|
await setThreadSession(thread.id, forkedSession.id)
|
|
101
|
+
const sourceWorkspace = await getThreadWorktreeOrWorkspace(sourceThread.id)
|
|
102
|
+
if (sourceWorkspace?.status === 'ready' && sourceWorkspace.workspace_directory) {
|
|
103
|
+
await createPendingWorkspace({
|
|
104
|
+
threadId: thread.id,
|
|
105
|
+
workspaceType: sourceWorkspace.workspace_type,
|
|
106
|
+
workspaceName: sourceWorkspace.workspace_name ?? '',
|
|
107
|
+
projectDirectory,
|
|
108
|
+
})
|
|
109
|
+
await setWorkspaceReady({
|
|
110
|
+
threadId: thread.id,
|
|
111
|
+
workspaceId: sourceWorkspace.workspace_id ?? undefined,
|
|
112
|
+
workspaceDirectory: sourceWorkspace.workspace_directory,
|
|
113
|
+
})
|
|
114
|
+
}
|
|
88
115
|
|
|
89
116
|
// Parallelize: member add and status message are independent best-effort actions
|
|
90
117
|
const sourceThreadLink = `<#${sourceThread.id}>`
|
|
91
118
|
await Promise.all([
|
|
92
|
-
thread.members.add(userId).catch(() => {
|
|
119
|
+
thread.members.add(userId).catch((error) => {
|
|
120
|
+
logger.warn('Could not add fork member:', error)
|
|
121
|
+
}),
|
|
93
122
|
sendThreadMessage(
|
|
94
123
|
thread,
|
|
95
124
|
`Reusing context from ${sourceThreadLink} to answer prompt...\n${prompt}`,
|
|
@@ -111,7 +140,7 @@ export async function forkSessionToBtwThread({
|
|
|
111
140
|
`Parent session: ${sessionId} (thread <#${sourceThread.id}>)`,
|
|
112
141
|
`Do NOT send messages to the parent session unless the user explicitly asks you to.`,
|
|
113
142
|
``,
|
|
114
|
-
|
|
143
|
+
modelPrompt,
|
|
115
144
|
].join('\n')
|
|
116
145
|
|
|
117
146
|
const runtime = getOrCreateRuntime({
|
|
@@ -121,13 +150,19 @@ export async function forkSessionToBtwThread({
|
|
|
121
150
|
sdkDirectory,
|
|
122
151
|
channelId,
|
|
123
152
|
appId,
|
|
153
|
+
sessionId: forkedSession.id,
|
|
124
154
|
})
|
|
125
155
|
await runtime.enqueueIncoming({
|
|
126
156
|
prompt: wrappedPrompt,
|
|
157
|
+
agent,
|
|
158
|
+
images,
|
|
127
159
|
userId,
|
|
128
160
|
username,
|
|
129
161
|
appId,
|
|
130
162
|
mode: 'opencode',
|
|
163
|
+
}).catch(async (error) => {
|
|
164
|
+
logger.error('Fork dispatch failed:', error)
|
|
165
|
+
await sendThreadMessage(thread, 'Could not send the request to OpenCode. Send your request again in this thread.')
|
|
131
166
|
})
|
|
132
167
|
|
|
133
168
|
return {
|