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
package/dist/commands/agent.js
CHANGED
|
@@ -2,18 +2,22 @@
|
|
|
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
|
import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ChannelType, ThreadAutoArchiveDuration, MessageFlags, } from 'discord.js';
|
|
6
7
|
import crypto from 'node:crypto';
|
|
7
|
-
import { setChannelAgent, setSessionAgent, clearSessionModel, getThreadSession, getSessionAgent, getChannelAgent, getChannelWorktreesEnabled, } from '../database.js';
|
|
8
|
+
import { setChannelAgent, setSessionAgent, setSessionModel, setChannelModel, getChannelModel, clearSessionModel, getThreadSession, getSessionAgent, getChannelAgent, getChannelWorktreesEnabled, } from '../database.js';
|
|
8
9
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
9
|
-
import { resolveTextChannel, resolveWorkingDirectory, getKimakiMetadata, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
10
|
-
import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js';
|
|
10
|
+
import { resolveTextChannel, resolveWorkingDirectory, resolveProjectDirectoryFromAutocomplete, getKimakiMetadata, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
11
|
+
import { getOrCreateRuntime, releaseCurrentThreadIngress, waitForCurrentThreadIngress, } from '../session-handler/thread-session-runtime.js';
|
|
11
12
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
12
13
|
import { formatModelSource, getCurrentModelInfo, } from './model.js';
|
|
13
14
|
import { isGitRepositoryRoot } from '../worktrees.js';
|
|
14
15
|
import { formatAutoWorktreeName, createWorktreeInBackground, worktreeCreatingMessage, } from './new-worktree.js';
|
|
15
16
|
import { WORKTREE_PREFIX } from './merge-worktree.js';
|
|
17
|
+
import { QUEUE_PREFIX } from '../message-formatting.js';
|
|
16
18
|
import { store } from '../store.js';
|
|
19
|
+
import { getThinkingValuesForModel, resolveRequestedThinkingVariant, } from '../thinking-utils.js';
|
|
20
|
+
import { OpenCodeSdkError } from '../errors.js';
|
|
17
21
|
const agentLogger = createLogger(LogPrefix.AGENT);
|
|
18
22
|
const AGENT_CONTEXT_TTL_MS = 10 * 60 * 1000;
|
|
19
23
|
const pendingAgentContexts = new Map();
|
|
@@ -161,6 +165,48 @@ export async function setAgentForContext({ context, agentName, }) {
|
|
|
161
165
|
agentLogger.log(`Set agent ${agentName} for channel ${context.channelId}`);
|
|
162
166
|
}
|
|
163
167
|
}
|
|
168
|
+
async function applyQuickAgentVariant({ context, agentName, variant, }) {
|
|
169
|
+
const modelInfo = await resolveAgentModelInfo({ context, agentName });
|
|
170
|
+
if (modelInfo.type === 'none') {
|
|
171
|
+
agentLogger.warn(`[AGENT] Skipping variant ${variant}: no model for agent ${agentName}`);
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
175
|
+
if (getClient instanceof Error)
|
|
176
|
+
return undefined;
|
|
177
|
+
const providersResponse = await getClient()
|
|
178
|
+
.provider.list({ directory: context.workingDirectory })
|
|
179
|
+
.catch((e) => new OpenCodeSdkError({ operation: 'provider.list', cause: e }));
|
|
180
|
+
if (providersResponse instanceof Error || !providersResponse.data) {
|
|
181
|
+
agentLogger.warn(`[AGENT] Skipping variant ${variant}: provider list failed`);
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const matchedVariant = resolveRequestedThinkingVariant({
|
|
185
|
+
requestedValue: variant,
|
|
186
|
+
providers: providersResponse.data.all,
|
|
187
|
+
providerId: modelInfo.providerID,
|
|
188
|
+
modelId: modelInfo.modelID,
|
|
189
|
+
});
|
|
190
|
+
if (!matchedVariant) {
|
|
191
|
+
agentLogger.warn(`[AGENT] Skipping variant ${variant}: not supported by ${modelInfo.model}`);
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
if (context.isThread && context.sessionId) {
|
|
195
|
+
await setSessionModel({
|
|
196
|
+
sessionId: context.sessionId,
|
|
197
|
+
modelId: modelInfo.model,
|
|
198
|
+
variant: matchedVariant,
|
|
199
|
+
});
|
|
200
|
+
return matchedVariant;
|
|
201
|
+
}
|
|
202
|
+
const existing = await getChannelModel(context.channelId);
|
|
203
|
+
await setChannelModel({
|
|
204
|
+
channelId: context.channelId,
|
|
205
|
+
modelId: existing?.modelId ?? modelInfo.model,
|
|
206
|
+
variant: matchedVariant,
|
|
207
|
+
});
|
|
208
|
+
return matchedVariant;
|
|
209
|
+
}
|
|
164
210
|
function formatAgentModelLine(modelInfo) {
|
|
165
211
|
if (modelInfo.type === 'none')
|
|
166
212
|
return '';
|
|
@@ -174,15 +220,16 @@ function formatAgentModelLine(modelInfo) {
|
|
|
174
220
|
}
|
|
175
221
|
return `${line}\nThis model comes from a ${modelInfo.type} override. Use /model and press Clear override if you want this agent's model.`;
|
|
176
222
|
}
|
|
177
|
-
function formatAgentPreferenceReply({ agentName, previousAgentName, modelInfo, scope, }) {
|
|
223
|
+
function formatAgentPreferenceReply({ agentName, previousAgentName, modelInfo, scope, variant, }) {
|
|
178
224
|
const sameAgent = previousAgentName === agentName;
|
|
179
225
|
const verb = sameAgent ? 'Using' : 'Switched to';
|
|
180
226
|
const previousText = !sameAgent && previousAgentName ? ` (was **${previousAgentName}**)` : '';
|
|
181
227
|
const modelText = formatAgentModelLine(modelInfo);
|
|
228
|
+
const variantText = variant ? `\nVariant: **${variant}**` : '';
|
|
182
229
|
if (scope === 'session') {
|
|
183
|
-
return `${verb} **${agentName}** agent for this session${previousText}${modelText}\nThe agent will change on the next message.`;
|
|
230
|
+
return `${verb} **${agentName}** agent for this session${previousText}${modelText}${variantText}\nThe agent will change on the next message.`;
|
|
184
231
|
}
|
|
185
|
-
return `${verb} **${agentName}** agent for this channel${previousText}${modelText}\nAll new sessions will use this agent.`;
|
|
232
|
+
return `${verb} **${agentName}** agent for this channel${previousText}${modelText}${variantText}\nAll new sessions will use this agent.`;
|
|
186
233
|
}
|
|
187
234
|
async function resolveAgentModelInfo({ context, agentName, }) {
|
|
188
235
|
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
@@ -296,7 +343,13 @@ export async function handleAgentSelectMenu(interaction) {
|
|
|
296
343
|
channelId: context.channelId,
|
|
297
344
|
});
|
|
298
345
|
const previousAgentName = previousAgent.type !== 'none' ? previousAgent.agent : undefined;
|
|
299
|
-
await
|
|
346
|
+
await waitForCurrentThreadIngress();
|
|
347
|
+
try {
|
|
348
|
+
await setAgentForContext({ context, agentName: selectedAgent });
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
releaseCurrentThreadIngress();
|
|
352
|
+
}
|
|
300
353
|
const modelInfo = await resolveAgentModelInfo({
|
|
301
354
|
context,
|
|
302
355
|
agentName: selectedAgent,
|
|
@@ -333,9 +386,16 @@ export async function handleAgentSelectMenu(interaction) {
|
|
|
333
386
|
export async function handleQuickAgentCommand({ command, appId, }) {
|
|
334
387
|
const fallbackAgentName = command.commandName.replace(/-agent$/, '');
|
|
335
388
|
const prompt = command.options.getString('prompt') || undefined;
|
|
389
|
+
const variant = command.options.getString('variant') || undefined;
|
|
336
390
|
// Prompt mode: send the prompt with this agent immediately.
|
|
337
391
|
if (prompt) {
|
|
338
|
-
return handleQuickAgentWithPrompt({
|
|
392
|
+
return handleQuickAgentWithPrompt({
|
|
393
|
+
command,
|
|
394
|
+
appId,
|
|
395
|
+
fallbackAgentName,
|
|
396
|
+
prompt,
|
|
397
|
+
variant,
|
|
398
|
+
});
|
|
339
399
|
}
|
|
340
400
|
// No prompt: switch the persistent agent preference (original behavior).
|
|
341
401
|
await command.deferReply();
|
|
@@ -356,20 +416,37 @@ export async function handleQuickAgentCommand({ command, appId, }) {
|
|
|
356
416
|
channelId: context.channelId,
|
|
357
417
|
});
|
|
358
418
|
const previousAgentName = previousAgent.type !== 'none' ? previousAgent.agent : undefined;
|
|
359
|
-
await
|
|
360
|
-
|
|
361
|
-
context,
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const scope = context.isThread && context.sessionId ? 'session' : 'channel';
|
|
365
|
-
await command.editReply({
|
|
366
|
-
content: formatAgentPreferenceReply({
|
|
419
|
+
await waitForCurrentThreadIngress();
|
|
420
|
+
try {
|
|
421
|
+
await setAgentForContext({ context, agentName: resolvedAgentName });
|
|
422
|
+
const modelInfo = await resolveAgentModelInfo({
|
|
423
|
+
context,
|
|
367
424
|
agentName: resolvedAgentName,
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
425
|
+
});
|
|
426
|
+
const appliedVariant = variant
|
|
427
|
+
? await applyQuickAgentVariant({
|
|
428
|
+
context,
|
|
429
|
+
agentName: resolvedAgentName,
|
|
430
|
+
variant,
|
|
431
|
+
})
|
|
432
|
+
: undefined;
|
|
433
|
+
const scope = context.isThread && context.sessionId ? 'session' : 'channel';
|
|
434
|
+
const unsupportedVariantNote = variant && !appliedVariant
|
|
435
|
+
? `\nVariant **${variant}** is not supported by this agent's model.`
|
|
436
|
+
: '';
|
|
437
|
+
await command.editReply({
|
|
438
|
+
content: `${formatAgentPreferenceReply({
|
|
439
|
+
agentName: resolvedAgentName,
|
|
440
|
+
previousAgentName,
|
|
441
|
+
modelInfo,
|
|
442
|
+
scope,
|
|
443
|
+
variant: appliedVariant,
|
|
444
|
+
})}${unsupportedVariantNote}`,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
finally {
|
|
448
|
+
releaseCurrentThreadIngress();
|
|
449
|
+
}
|
|
373
450
|
}
|
|
374
451
|
catch (error) {
|
|
375
452
|
agentLogger.error('Error in quick agent command:', error);
|
|
@@ -384,7 +461,7 @@ export async function handleQuickAgentCommand({ command, appId, }) {
|
|
|
384
461
|
* In a channel: create a new thread whose session starts with the requested agent.
|
|
385
462
|
* Channel-level preferences are not changed.
|
|
386
463
|
*/
|
|
387
|
-
async function handleQuickAgentWithPrompt({ command, appId, fallbackAgentName, prompt, }) {
|
|
464
|
+
async function handleQuickAgentWithPrompt({ command, appId, fallbackAgentName, prompt, variant, }) {
|
|
388
465
|
const channel = command.channel;
|
|
389
466
|
if (!channel) {
|
|
390
467
|
await command.reply({
|
|
@@ -422,7 +499,7 @@ async function handleQuickAgentWithPrompt({ command, appId, fallbackAgentName, p
|
|
|
422
499
|
});
|
|
423
500
|
// Visible reply showing the one-shot prompt (not ephemeral, so it appears in thread).
|
|
424
501
|
await command.reply({
|
|
425
|
-
content:
|
|
502
|
+
content: `${QUEUE_PREFIX}**${command.user.displayName}** (${resolvedAgentName}): ${displayText}`,
|
|
426
503
|
flags: SILENT_MESSAGE_FLAGS,
|
|
427
504
|
});
|
|
428
505
|
await runtime.enqueueIncoming({
|
|
@@ -432,6 +509,7 @@ async function handleQuickAgentWithPrompt({ command, appId, fallbackAgentName, p
|
|
|
432
509
|
agent: resolvedAgentName,
|
|
433
510
|
appId,
|
|
434
511
|
mode: 'opencode',
|
|
512
|
+
variant,
|
|
435
513
|
});
|
|
436
514
|
}
|
|
437
515
|
else if (channel.type === ChannelType.GuildText) {
|
|
@@ -459,7 +537,7 @@ async function handleQuickAgentWithPrompt({ command, appId, fallbackAgentName, p
|
|
|
459
537
|
? `${WORKTREE_PREFIX}${baseThreadName}`
|
|
460
538
|
: baseThreadName;
|
|
461
539
|
const starterMessage = await channel.send({
|
|
462
|
-
content:
|
|
540
|
+
content: `${QUEUE_PREFIX}**${command.user.displayName}** (${resolvedAgentName}): ${displayText}`,
|
|
463
541
|
flags: SILENT_MESSAGE_FLAGS,
|
|
464
542
|
});
|
|
465
543
|
const thread = await starterMessage.startThread({
|
|
@@ -487,14 +565,12 @@ async function handleQuickAgentWithPrompt({ command, appId, fallbackAgentName, p
|
|
|
487
565
|
rest: command.client.rest,
|
|
488
566
|
});
|
|
489
567
|
}
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
return result;
|
|
497
|
-
})();
|
|
568
|
+
const worktreeResult = worktreePromise ? await worktreePromise : projectDirectory;
|
|
569
|
+
if (worktreeResult instanceof Error) {
|
|
570
|
+
await command.editReply(`Worktree creation failed: ${worktreeResult.message}`);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const sessionDirectory = worktreeResult;
|
|
498
574
|
await command
|
|
499
575
|
.editReply(`Sent with **${resolvedAgentName}** agent in ${thread.toString()}`)
|
|
500
576
|
.catch(() => {
|
|
@@ -515,6 +591,7 @@ async function handleQuickAgentWithPrompt({ command, appId, fallbackAgentName, p
|
|
|
515
591
|
agent: resolvedAgentName,
|
|
516
592
|
appId,
|
|
517
593
|
mode: 'opencode',
|
|
594
|
+
variant,
|
|
518
595
|
});
|
|
519
596
|
}
|
|
520
597
|
else {
|
|
@@ -524,3 +601,58 @@ async function handleQuickAgentWithPrompt({ command, appId, fallbackAgentName, p
|
|
|
524
601
|
});
|
|
525
602
|
}
|
|
526
603
|
}
|
|
604
|
+
export async function handleQuickAgentAutocomplete({ interaction, }) {
|
|
605
|
+
if (interaction.options.getFocused(true).name !== 'variant') {
|
|
606
|
+
await interaction.respond([]);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const focusedValue = interaction.options.getFocused().trim().toLowerCase();
|
|
610
|
+
const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction);
|
|
611
|
+
if (!projectDirectory) {
|
|
612
|
+
await interaction.respond([]);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
616
|
+
if (getClient instanceof Error) {
|
|
617
|
+
await interaction.respond([]);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const [providersResponse, agentsResponse] = await Promise.all([
|
|
621
|
+
getClient()
|
|
622
|
+
.provider.list({ directory: projectDirectory })
|
|
623
|
+
.catch((e) => new OpenCodeSdkError({ operation: 'provider.list', cause: e })),
|
|
624
|
+
getClient()
|
|
625
|
+
.app.agents({ directory: projectDirectory })
|
|
626
|
+
.catch((e) => new OpenCodeSdkError({ operation: 'app.agents', cause: e })),
|
|
627
|
+
]);
|
|
628
|
+
if (providersResponse instanceof Error || agentsResponse instanceof Error) {
|
|
629
|
+
agentLogger.warn('[AUTOCOMPLETE] Failed to fetch variant choices');
|
|
630
|
+
await interaction.respond([]);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (!providersResponse.data || !agentsResponse.data) {
|
|
634
|
+
await interaction.respond([]);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const resolvedAgentName = parseQuickAgentNameFromDescription(interaction.command?.description) ||
|
|
638
|
+
interaction.commandName.replace(/-agent$/, '');
|
|
639
|
+
const agent = agentsResponse.data.find((candidate) => candidate.name === resolvedAgentName);
|
|
640
|
+
const agentModel = agent?.model;
|
|
641
|
+
if (!agentModel) {
|
|
642
|
+
await interaction.respond([]);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const variants = getThinkingValuesForModel({
|
|
646
|
+
providers: providersResponse.data.all,
|
|
647
|
+
providerId: agentModel.providerID,
|
|
648
|
+
modelId: agentModel.modelID,
|
|
649
|
+
}).filter((variant) => {
|
|
650
|
+
if (!focusedValue)
|
|
651
|
+
return true;
|
|
652
|
+
return variant.toLowerCase().includes(focusedValue);
|
|
653
|
+
});
|
|
654
|
+
await interaction.respond(variants.slice(0, 25).map((variant) => ({
|
|
655
|
+
name: variant.slice(0, 100),
|
|
656
|
+
value: variant,
|
|
657
|
+
})));
|
|
658
|
+
}
|
|
@@ -5,11 +5,14 @@ import { StringSelectMenuBuilder, StringSelectMenuInteraction, ActionRowBuilder,
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
7
7
|
import { getOpencodeClient } from '../opencode.js';
|
|
8
|
+
import { DiscordOperationError } from '../errors.js';
|
|
8
9
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
10
|
+
import { QUEUE_PREFIX } from '../message-formatting.js';
|
|
9
11
|
const logger = createLogger(LogPrefix.ASK_QUESTION);
|
|
10
12
|
// Store pending question contexts by hash.
|
|
11
|
-
// TTL
|
|
12
|
-
|
|
13
|
+
// No TTL on purpose: a question can only be answered by the user, so expiring it
|
|
14
|
+
// could only abort the run. Cleanup happens on answer, on a new user message, on
|
|
15
|
+
// abort (abortActiveRunInternal), and on thread dispose.
|
|
13
16
|
export const pendingQuestionContexts = new Map();
|
|
14
17
|
export function areAllQuestionsAnswered({ totalQuestions, answers, }) {
|
|
15
18
|
for (let i = 0; i < totalQuestions; i++) {
|
|
@@ -75,32 +78,6 @@ export async function showAskUserQuestionDropdowns({ thread, sessionId, director
|
|
|
75
78
|
contextHash,
|
|
76
79
|
};
|
|
77
80
|
pendingQuestionContexts.set(contextHash, context);
|
|
78
|
-
// On TTL expiry: hide the dropdown UI and abort the session so OpenCode
|
|
79
|
-
// unblocks. We intentionally do NOT call question.reply() — sending 'Other'
|
|
80
|
-
// made the model think the user chose an option when they didn't.
|
|
81
|
-
setTimeout(async () => {
|
|
82
|
-
const ctx = pendingQuestionContexts.get(contextHash);
|
|
83
|
-
if (!ctx) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
// Delete context first so the dropdown becomes inert immediately.
|
|
87
|
-
// Without this, a user clicking during the abort() await would still
|
|
88
|
-
// be accepted by handleAskQuestionSelectMenu, then abort() would
|
|
89
|
-
// kill that valid run.
|
|
90
|
-
deletePendingQuestionContextsForRequest({
|
|
91
|
-
threadId: ctx.thread.id,
|
|
92
|
-
requestId: ctx.requestId,
|
|
93
|
-
});
|
|
94
|
-
// Abort the session so OpenCode isn't stuck waiting for a reply
|
|
95
|
-
const client = getOpencodeClient(ctx.directory);
|
|
96
|
-
if (client) {
|
|
97
|
-
await client.session.abort({
|
|
98
|
-
sessionID: ctx.sessionId,
|
|
99
|
-
}).catch((error) => {
|
|
100
|
-
logger.error('Failed to abort session after question expiry:', error);
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
}, QUESTION_CONTEXT_TTL_MS).unref();
|
|
104
81
|
// Send one message per question with its dropdown directly underneath
|
|
105
82
|
for (let i = 0; i < input.questions.length; i++) {
|
|
106
83
|
const q = input.questions[i];
|
|
@@ -129,11 +106,24 @@ export async function showAskUserQuestionDropdowns({ thread, sessionId, director
|
|
|
129
106
|
selectMenu.setMaxValues(options.length);
|
|
130
107
|
}
|
|
131
108
|
const actionRow = new ActionRowBuilder().addComponents(selectMenu);
|
|
132
|
-
await thread.send({
|
|
109
|
+
const sendResult = await thread.send({
|
|
133
110
|
content: `**${(q.header || '').slice(0, 200)}**\n${q.question.slice(0, 1700)}`,
|
|
134
111
|
components: [actionRow],
|
|
135
112
|
flags: silent ? SILENT_MESSAGE_FLAGS : NOTIFY_MESSAGE_FLAGS,
|
|
136
|
-
});
|
|
113
|
+
}).catch((e) => new DiscordOperationError({ operation: 'sendQuestionDropdown', cause: e }));
|
|
114
|
+
// Without a visible dropdown the user can never answer, and there is no TTL
|
|
115
|
+
// to rescue the run, so drop the context and abort so OpenCode unblocks.
|
|
116
|
+
if (sendResult instanceof Error) {
|
|
117
|
+
logger.error('Failed to send question dropdown:', sendResult);
|
|
118
|
+
deletePendingQuestionContextsForRequest({ threadId: thread.id, requestId });
|
|
119
|
+
const client = getOpencodeClient(directory);
|
|
120
|
+
if (client) {
|
|
121
|
+
await client.session.abort({ sessionID: sessionId }).catch((error) => {
|
|
122
|
+
logger.error('Failed to abort session after question send failure:', error);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
137
127
|
}
|
|
138
128
|
logger.log(`Showed ${input.questions.length} question dropdown(s) for session ${sessionId}`);
|
|
139
129
|
}
|
|
@@ -194,7 +184,7 @@ export async function handleAskQuestionSelectMenu(interaction) {
|
|
|
194
184
|
components: [], // Remove the dropdown
|
|
195
185
|
});
|
|
196
186
|
const username = interaction.user.globalName || interaction.user.username;
|
|
197
|
-
await sendThreadMessage(context.thread,
|
|
187
|
+
await sendThreadMessage(context.thread, `${QUEUE_PREFIX}**${username}:** ${answeredText}`);
|
|
198
188
|
// Check if all questions are answered
|
|
199
189
|
if (areAllQuestionsAnswered(context)) {
|
|
200
190
|
// All questions answered - send result back to session
|
|
@@ -276,7 +266,7 @@ export function parseAskUserQuestionTool(part) {
|
|
|
276
266
|
* the tool answer so the model sees the user's response. The caller should
|
|
277
267
|
* NOT also enqueue the message as a new prompt.
|
|
278
268
|
* Returns 'replied' on success, 'reply-failed' if the reply call fails
|
|
279
|
-
* (context kept pending so
|
|
269
|
+
* (context kept pending so the user can retry).
|
|
280
270
|
*/
|
|
281
271
|
export async function cancelPendingQuestion(threadId, userMessage) {
|
|
282
272
|
// Find pending question for this thread
|
|
@@ -320,7 +310,7 @@ export async function cancelPendingQuestion(threadId, userMessage) {
|
|
|
320
310
|
}
|
|
321
311
|
catch (error) {
|
|
322
312
|
logger.error('Failed to answer question:', error);
|
|
323
|
-
// Keep context pending so
|
|
313
|
+
// Keep context pending so the user can retry.
|
|
324
314
|
// Caller should not consume the user message since reply failed.
|
|
325
315
|
return 'reply-failed';
|
|
326
316
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// Tests AskUserQuestion request deduplication and cleanup helpers.
|
|
2
2
|
import { afterEach, describe, expect, test, vi } from 'vitest';
|
|
3
3
|
import { areAllQuestionsAnswered, deletePendingQuestionContextsForRequest, pendingQuestionContexts, showAskUserQuestionDropdowns, } from './ask-question.js';
|
|
4
|
-
function createFakeThread() {
|
|
4
|
+
function createFakeThread({ failSend } = {}) {
|
|
5
5
|
const send = vi.fn(async () => {
|
|
6
|
+
if (failSend) {
|
|
7
|
+
throw new Error('Missing Permissions');
|
|
8
|
+
}
|
|
6
9
|
return { id: 'msg-1' };
|
|
7
10
|
});
|
|
8
11
|
return {
|
|
@@ -88,6 +91,45 @@ describe('ask-question', () => {
|
|
|
88
91
|
expect(removed).toBe(2);
|
|
89
92
|
expect([...pendingQuestionContexts.keys()]).toEqual(['ctx-3']);
|
|
90
93
|
});
|
|
94
|
+
test('does not schedule a question expiry timer', async () => {
|
|
95
|
+
const timeoutSpy = vi.spyOn(globalThis, 'setTimeout');
|
|
96
|
+
await showAskUserQuestionDropdowns({
|
|
97
|
+
thread: createFakeThread(),
|
|
98
|
+
sessionId: 'ses-1',
|
|
99
|
+
directory: '/project',
|
|
100
|
+
requestId: 'req-no-ttl',
|
|
101
|
+
input: {
|
|
102
|
+
questions: [{
|
|
103
|
+
question: 'Choose one',
|
|
104
|
+
header: 'Pick',
|
|
105
|
+
options: [
|
|
106
|
+
{ label: 'Alpha', description: 'A' },
|
|
107
|
+
{ label: 'Beta', description: 'B' },
|
|
108
|
+
],
|
|
109
|
+
}],
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
expect(timeoutSpy).not.toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
test('drops the context when the dropdown message fails to send', async () => {
|
|
115
|
+
await showAskUserQuestionDropdowns({
|
|
116
|
+
thread: createFakeThread({ failSend: true }),
|
|
117
|
+
sessionId: 'ses-1',
|
|
118
|
+
directory: '/project',
|
|
119
|
+
requestId: 'req-send-fails',
|
|
120
|
+
input: {
|
|
121
|
+
questions: [{
|
|
122
|
+
question: 'Choose one',
|
|
123
|
+
header: 'Pick',
|
|
124
|
+
options: [
|
|
125
|
+
{ label: 'Alpha', description: 'A' },
|
|
126
|
+
{ label: 'Beta', description: 'B' },
|
|
127
|
+
],
|
|
128
|
+
}],
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
expect(pendingQuestionContexts.size).toBe(0);
|
|
132
|
+
});
|
|
91
133
|
test('requires every question to have an answer', () => {
|
|
92
134
|
expect(areAllQuestionsAnswered({
|
|
93
135
|
totalQuestions: 3,
|
package/dist/commands/btw.js
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
// a new thread, forks the entire session (no messageID), and immediately
|
|
4
4
|
// dispatches the user's prompt so the forked session starts working right away.
|
|
5
5
|
import { ChannelType, ThreadAutoArchiveDuration, MessageFlags, } from 'discord.js';
|
|
6
|
-
import { getThreadSession, setThreadSession } from '../database.js';
|
|
6
|
+
import { getThreadSession, setThreadSession, getThreadWorktreeOrWorkspace, createPendingWorkspace, setWorkspaceReady, } from '../database.js';
|
|
7
7
|
import { resolveWorkingDirectory, resolveTextChannel, sendThreadMessage, } from '../discord-utils.js';
|
|
8
8
|
import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js';
|
|
9
9
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
10
10
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
11
11
|
import { copyCurrentSessionModel } from './model.js';
|
|
12
12
|
const logger = createLogger(LogPrefix.FORK);
|
|
13
|
-
export async function forkSessionToBtwThread({ sourceThread, projectDirectory, sdkDirectory, prompt, userId, username, appId, }) {
|
|
13
|
+
export async function forkSessionToBtwThread({ sourceThread, projectDirectory, sdkDirectory, prompt, modelPrompt = prompt, userId, username, appId, agent, images, }) {
|
|
14
14
|
// Parallelize: session lookup + opencode init + parent channel resolve are independent
|
|
15
15
|
const [sessionId, getClientResult, textChannel] = await Promise.all([
|
|
16
16
|
getThreadSession(sourceThread.id),
|
|
@@ -29,7 +29,7 @@ export async function forkSessionToBtwThread({ sourceThread, projectDirectory, s
|
|
|
29
29
|
return new Error('Could not resolve parent text channel');
|
|
30
30
|
}
|
|
31
31
|
// Fork must succeed before creating the Discord thread to avoid orphan threads
|
|
32
|
-
const forkResponse = await getClientResult().session.fork({ sessionID: sessionId });
|
|
32
|
+
const forkResponse = await getClientResult().session.fork({ sessionID: sessionId, directory: sdkDirectory });
|
|
33
33
|
if (!forkResponse.data) {
|
|
34
34
|
return new Error('Failed to fork session');
|
|
35
35
|
}
|
|
@@ -41,7 +41,7 @@ export async function forkSessionToBtwThread({ sourceThread, projectDirectory, s
|
|
|
41
41
|
channelId,
|
|
42
42
|
appId,
|
|
43
43
|
getClient: getClientResult,
|
|
44
|
-
directory:
|
|
44
|
+
directory: sdkDirectory,
|
|
45
45
|
});
|
|
46
46
|
const thread = await textChannel.threads.create({
|
|
47
47
|
name: `btw: ${prompt}`.slice(0, 100),
|
|
@@ -50,10 +50,26 @@ export async function forkSessionToBtwThread({ sourceThread, projectDirectory, s
|
|
|
50
50
|
});
|
|
51
51
|
// DB mapping must complete before user-visible actions so the thread is routable
|
|
52
52
|
await setThreadSession(thread.id, forkedSession.id);
|
|
53
|
+
const sourceWorkspace = await getThreadWorktreeOrWorkspace(sourceThread.id);
|
|
54
|
+
if (sourceWorkspace?.status === 'ready' && sourceWorkspace.workspace_directory) {
|
|
55
|
+
await createPendingWorkspace({
|
|
56
|
+
threadId: thread.id,
|
|
57
|
+
workspaceType: sourceWorkspace.workspace_type,
|
|
58
|
+
workspaceName: sourceWorkspace.workspace_name ?? '',
|
|
59
|
+
projectDirectory,
|
|
60
|
+
});
|
|
61
|
+
await setWorkspaceReady({
|
|
62
|
+
threadId: thread.id,
|
|
63
|
+
workspaceId: sourceWorkspace.workspace_id ?? undefined,
|
|
64
|
+
workspaceDirectory: sourceWorkspace.workspace_directory,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
53
67
|
// Parallelize: member add and status message are independent best-effort actions
|
|
54
68
|
const sourceThreadLink = `<#${sourceThread.id}>`;
|
|
55
69
|
await Promise.all([
|
|
56
|
-
thread.members.add(userId).catch(() => {
|
|
70
|
+
thread.members.add(userId).catch((error) => {
|
|
71
|
+
logger.warn('Could not add fork member:', error);
|
|
72
|
+
}),
|
|
57
73
|
sendThreadMessage(thread, `Reusing context from ${sourceThreadLink} to answer prompt...\n${prompt}`),
|
|
58
74
|
]);
|
|
59
75
|
logger.log(`Created btw fork session ${forkedSession.id} in thread ${thread.id} from source thread ${sourceThread.id} (session ${sessionId})`);
|
|
@@ -68,7 +84,7 @@ export async function forkSessionToBtwThread({ sourceThread, projectDirectory, s
|
|
|
68
84
|
`Parent session: ${sessionId} (thread <#${sourceThread.id}>)`,
|
|
69
85
|
`Do NOT send messages to the parent session unless the user explicitly asks you to.`,
|
|
70
86
|
``,
|
|
71
|
-
|
|
87
|
+
modelPrompt,
|
|
72
88
|
].join('\n');
|
|
73
89
|
const runtime = getOrCreateRuntime({
|
|
74
90
|
threadId: thread.id,
|
|
@@ -77,13 +93,19 @@ export async function forkSessionToBtwThread({ sourceThread, projectDirectory, s
|
|
|
77
93
|
sdkDirectory,
|
|
78
94
|
channelId,
|
|
79
95
|
appId,
|
|
96
|
+
sessionId: forkedSession.id,
|
|
80
97
|
});
|
|
81
98
|
await runtime.enqueueIncoming({
|
|
82
99
|
prompt: wrappedPrompt,
|
|
100
|
+
agent,
|
|
101
|
+
images,
|
|
83
102
|
userId,
|
|
84
103
|
username,
|
|
85
104
|
appId,
|
|
86
105
|
mode: 'opencode',
|
|
106
|
+
}).catch(async (error) => {
|
|
107
|
+
logger.error('Fork dispatch failed:', error);
|
|
108
|
+
await sendThreadMessage(thread, 'Could not send the request to OpenCode. Send your request again in this thread.');
|
|
87
109
|
});
|
|
88
110
|
return {
|
|
89
111
|
thread,
|
|
@@ -12,6 +12,7 @@ import path from 'node:path';
|
|
|
12
12
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
13
13
|
import { notifyError } from '../sentry.js';
|
|
14
14
|
import { NOTIFY_MESSAGE_FLAGS, sendThreadMessage } from '../discord-utils.js';
|
|
15
|
+
import { QUEUE_PREFIX } from '../message-formatting.js';
|
|
15
16
|
const logger = createLogger(LogPrefix.FILE_UPLOAD);
|
|
16
17
|
// 5 minute TTL for pending contexts - if user doesn't click within this time,
|
|
17
18
|
// clean up the context and resolve with empty array to unblock the plugin tool
|
|
@@ -221,7 +222,7 @@ export async function handleFileUploadModalSubmit(interaction) {
|
|
|
221
222
|
await interaction.editReply({ content: summary });
|
|
222
223
|
if (downloadedPaths.length > 0) {
|
|
223
224
|
const username = interaction.user.globalName || interaction.user.username;
|
|
224
|
-
await sendThreadMessage(context.thread,
|
|
225
|
+
await sendThreadMessage(context.thread, `${QUEUE_PREFIX}**${username}:** Uploaded ${fileNames.join(', ')}`);
|
|
225
226
|
}
|
|
226
227
|
resolveContext(context, downloadedPaths);
|
|
227
228
|
logger.log(`File upload completed for session ${context.sessionId}: ${downloadedPaths.length} files`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// /fork-subagent command - Fork a subagent task session into a new thread.
|
|
2
2
|
import { ActionRowBuilder, MessageFlags, StringSelectMenuBuilder, ThreadAutoArchiveDuration, } from 'discord.js';
|
|
3
3
|
import { getSessionEventSnapshot, getThreadSession, setThreadSession, } from '../database.js';
|
|
4
|
-
import { resolveTextChannel, resolveWorkingDirectory, sendThreadMessage, } from '../discord-utils.js';
|
|
4
|
+
import { resolveTextChannel, resolveWorkingDirectory, sendThreadMessage, sendSessionPartBatches, } from '../discord-utils.js';
|
|
5
5
|
import { collectSessionChunks, batchChunksForDiscord, } from '../message-formatting.js';
|
|
6
6
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
7
7
|
import { getDerivedSubagentSessions, } from '../session-handler/event-stream-state.js';
|
|
@@ -163,9 +163,7 @@ export async function handleForkSubagentSelectMenu(interaction) {
|
|
|
163
163
|
limit: 30,
|
|
164
164
|
});
|
|
165
165
|
const batched = batchChunksForDiscord(chunks);
|
|
166
|
-
|
|
167
|
-
await sendThreadMessage(forkedThread, batch.content);
|
|
168
|
-
}
|
|
166
|
+
await sendSessionPartBatches({ thread: forkedThread, batches: batched });
|
|
169
167
|
}
|
|
170
168
|
}
|
|
171
169
|
catch (error) {
|
package/dist/commands/fork.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ChannelType, ThreadAutoArchiveDuration, MessageFlags, } from 'discord.js';
|
|
3
3
|
import { getThreadSession, setThreadSession, setPartMessagesBatch, } from '../database.js';
|
|
4
4
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
|
-
import { resolveWorkingDirectory, resolveTextChannel, sendThreadMessage, } from '../discord-utils.js';
|
|
5
|
+
import { resolveWorkingDirectory, resolveTextChannel, sendThreadMessage, sendSessionPartBatches, } from '../discord-utils.js';
|
|
6
6
|
import { collectSessionChunks, batchChunksForDiscord, } from '../message-formatting.js';
|
|
7
7
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
8
8
|
import * as errore from 'errore';
|
|
@@ -240,11 +240,11 @@ export async function handleForkSelectMenu(interaction) {
|
|
|
240
240
|
limit: 30,
|
|
241
241
|
});
|
|
242
242
|
const batched = batchChunksForDiscord(chunks);
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
const sent = await sendSessionPartBatches({ thread, batches: batched });
|
|
244
|
+
for (const batch of sent) {
|
|
245
245
|
await setPartMessagesBatch(batch.partIds.map((partId) => ({
|
|
246
246
|
partId,
|
|
247
|
-
messageId:
|
|
247
|
+
messageId: batch.message.id,
|
|
248
248
|
threadId: thread.id,
|
|
249
249
|
})));
|
|
250
250
|
}
|
|
@@ -104,16 +104,23 @@ export async function handleLastSessionsCommand({ command, }) {
|
|
|
104
104
|
}
|
|
105
105
|
const tableMarkdown = buildSessionTable({ rows });
|
|
106
106
|
const segments = splitTablesFromMarkdown(tableMarkdown);
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
107
|
+
const btwNote = {
|
|
108
|
+
type: ComponentType.TextDisplay,
|
|
109
|
+
content: 'Titles prefixed `btw:` are side sessions that answer a related question. They are not duplicate sessions.',
|
|
110
|
+
};
|
|
111
|
+
const allComponents = [
|
|
112
|
+
btwNote,
|
|
113
|
+
...segments.flatMap((segment) => {
|
|
114
|
+
if (segment.type === 'components') {
|
|
115
|
+
return segment.components;
|
|
116
|
+
}
|
|
117
|
+
const textDisplay = {
|
|
118
|
+
type: ComponentType.TextDisplay,
|
|
119
|
+
content: segment.text,
|
|
120
|
+
};
|
|
121
|
+
return [textDisplay];
|
|
122
|
+
}),
|
|
123
|
+
];
|
|
117
124
|
const { components } = truncateComponents(allComponents);
|
|
118
125
|
await command.editReply({
|
|
119
126
|
components,
|