kimaki 0.27.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 +247 -64
- package/dist/anthropic-account-identity.js +8 -1
- package/dist/anthropic-auth-plugin.js +9 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +6 -1
- package/dist/cli-commands/session.js +139 -82
- package/dist/cli-commands/task.js +21 -14
- package/dist/cli-parsing.test.js +24 -1
- package/dist/cli-runner.js +16 -4
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +158 -24
- package/dist/commands/ask-question.js +2 -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/merge-worktree.js +13 -6
- package/dist/commands/model-variant.js +3 -2
- package/dist/commands/model.js +4 -3
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +28 -0
- package/dist/db.js +11 -0
- package/dist/db.test.js +100 -0
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +501 -474
- package/dist/discord-command-registration.js +16 -9
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +84 -0
- package/dist/discord-utils.test.js +135 -1
- package/dist/errors.js +5 -0
- 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/gateway-proxy.e2e.test.js +18 -18
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +12 -7
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +2 -2
- package/dist/kimaki-opencode-plugin.js +2 -0
- package/dist/logger.js +1 -0
- package/dist/message-formatting.js +163 -44
- package/dist/message-formatting.test.js +473 -13
- package/dist/message-preprocessing.js +105 -1
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +49 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +29 -3
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +1 -1
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +5 -5
- 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-footer.e2e.test.js +29 -32
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +9 -7
- package/dist/queue-advanced-question.e2e.test.js +2 -2
- 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 +20 -20
- package/dist/queue-question-select-drain.e2e.test.js +7 -7
- package/dist/runtime-lifecycle.e2e.test.js +16 -19
- package/dist/schema.js +1 -1
- package/dist/session-handler/event-stream-state.js +124 -0
- package/dist/session-handler/event-stream-state.test.js +322 -1
- package/dist/session-handler/model-utils.js +16 -3
- package/dist/session-handler/model-utils.test.js +103 -3
- 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 +340 -135
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-title-rename.test.js +30 -0
- package/dist/sleep.e2e.test.js +14 -11
- package/dist/system-message.js +45 -12
- package/dist/system-message.test.js +117 -10
- package/dist/task-runner.js +2 -1
- package/dist/task-schedule.js +32 -2
- package/dist/task-schedule.test.js +79 -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 -54
- package/dist/undo-redo.e2e.test.js +17 -9
- 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 +41 -1
- package/dist/worktree-lifecycle.e2e.test.js +30 -30
- package/package.json +8 -8
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +56 -232
- package/skills/holocron/SKILL.md +117 -17
- package/src/agent-model.e2e.test.ts +293 -66
- package/src/anthropic-account-identity.ts +19 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +11 -16
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +6 -1
- package/src/cli-commands/session.ts +171 -105
- package/src/cli-commands/task.ts +22 -15
- package/src/cli-parsing.test.ts +34 -1
- package/src/cli-runner.ts +16 -3
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +199 -23
- package/src/commands/ask-question.ts +2 -1
- 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/merge-worktree.ts +13 -6
- package/src/commands/model-variant.ts +3 -2
- package/src/commands/model.ts +4 -3
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +40 -0
- package/src/db.test.ts +105 -0
- package/src/db.ts +12 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +43 -7
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +27 -11
- package/src/discord-utils.test.ts +157 -0
- package/src/discord-utils.ts +131 -1
- package/src/errors.ts +7 -0
- 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/gateway-proxy.e2e.test.ts +18 -17
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +16 -9
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +5 -5
- package/src/kimaki-opencode-plugin.ts +2 -0
- package/src/logger.ts +1 -0
- package/src/message-formatting.test.ts +586 -13
- package/src/message-formatting.ts +223 -46
- package/src/message-preprocessing.ts +116 -1
- package/src/opencode-command.test.ts +59 -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 +44 -4
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +1 -1
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +5 -5
- 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-footer.e2e.test.ts +30 -31
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +9 -7
- package/src/queue-advanced-question.e2e.test.ts +2 -2
- 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 +20 -20
- package/src/queue-question-select-drain.e2e.test.ts +7 -7
- package/src/runtime-lifecycle.e2e.test.ts +16 -18
- package/src/schema.sql +1 -1
- package/src/schema.ts +1 -1
- package/src/session-handler/event-stream-state.test.ts +353 -0
- package/src/session-handler/event-stream-state.ts +179 -0
- package/src/session-handler/model-utils.test.ts +125 -3
- package/src/session-handler/model-utils.ts +36 -4
- 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 +427 -158
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-title-rename.test.ts +45 -0
- package/src/sleep.e2e.test.ts +14 -11
- package/src/store.ts +1 -0
- package/src/system-message.test.ts +141 -10
- package/src/system-message.ts +66 -11
- package/src/task-runner.ts +2 -1
- package/src/task-schedule.test.ts +94 -0
- package/src/task-schedule.ts +47 -2
- 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 -53
- package/src/undo-redo.e2e.test.ts +17 -9
- 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 +44 -0
- package/src/voice.ts +34 -6
- package/src/worktree-lifecycle.e2e.test.ts +30 -30
|
@@ -319,9 +319,9 @@ describe('kimaki send --channel thread creation', () => {
|
|
|
319
319
|
"--- from: user (cli-send-tester)
|
|
320
320
|
thread empty content seed
|
|
321
321
|
--- from: assistant (TestBot)
|
|
322
|
-
*using deterministic-provider/deterministic-v2*
|
|
323
|
-
|
|
324
|
-
*project ⋅ main ⋅
|
|
322
|
+
> *using deterministic-provider/deterministic-v2*
|
|
323
|
+
> caught-by-model
|
|
324
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000830>
|
|
325
325
|
--- from: user (cli-send-tester)
|
|
326
326
|
--- from: assistant (TestBot)
|
|
327
327
|
I can see you sent a message, but Discord did not include its text.
|
|
@@ -436,13 +436,13 @@ describe('kimaki send --channel thread creation', () => {
|
|
|
436
436
|
}));
|
|
437
437
|
// Add test user to thread
|
|
438
438
|
await botClient.rest.put(Routes.threadMembers(threadData.id, TEST_USER_ID));
|
|
439
|
-
// Wait for the bot to reply
|
|
440
|
-
//
|
|
439
|
+
// Wait for the bot to reply (proves ThreadCreate handler picked up
|
|
440
|
+
// the starter message and started a session)
|
|
441
441
|
await waitForBotMessageContaining({
|
|
442
442
|
discord,
|
|
443
443
|
threadId: threadData.id,
|
|
444
444
|
userId: discord.botUserId,
|
|
445
|
-
text: '
|
|
445
|
+
text: 'ok',
|
|
446
446
|
timeout: 4_000,
|
|
447
447
|
});
|
|
448
448
|
// Wait for footer message (proves session completed successfully)
|
|
@@ -450,7 +450,7 @@ describe('kimaki send --channel thread creation', () => {
|
|
|
450
450
|
discord,
|
|
451
451
|
threadId: threadData.id,
|
|
452
452
|
timeout: 4_000,
|
|
453
|
-
afterMessageIncludes: '
|
|
453
|
+
afterMessageIncludes: 'ok',
|
|
454
454
|
afterAuthorId: discord.botUserId,
|
|
455
455
|
});
|
|
456
456
|
// Verify no DiscordAPIError[160004] or other errors in the thread.
|
|
@@ -461,9 +461,9 @@ describe('kimaki send --channel thread creation', () => {
|
|
|
461
461
|
return m.content.includes('Error:') || m.content.includes('160004');
|
|
462
462
|
});
|
|
463
463
|
expect(errorMessages).toHaveLength(0);
|
|
464
|
-
// Verify at least one
|
|
464
|
+
// Verify at least one reply exists (session produced output)
|
|
465
465
|
const botReplies = messages.filter((m) => {
|
|
466
|
-
return
|
|
466
|
+
return m.author.id === discord.botUserId;
|
|
467
467
|
});
|
|
468
468
|
expect(botReplies.length).toBeGreaterThanOrEqual(1);
|
|
469
469
|
}, 15_000);
|
|
@@ -6,6 +6,7 @@ import crypto from 'node:crypto';
|
|
|
6
6
|
import { getThreadSession } from '../database.js';
|
|
7
7
|
import { NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS, resolveWorkingDirectory, sendThreadMessage, } from '../discord-utils.js';
|
|
8
8
|
import { createLogger } from '../logger.js';
|
|
9
|
+
import { QUEUE_PREFIX } from '../message-formatting.js';
|
|
9
10
|
import { notifyError } from '../sentry.js';
|
|
10
11
|
import { getOrCreateRuntime, } from '../session-handler/thread-session-runtime.js';
|
|
11
12
|
const logger = createLogger('ACT_BTN');
|
|
@@ -214,7 +215,7 @@ export async function handleActionButton(interaction) {
|
|
|
214
215
|
});
|
|
215
216
|
const username = interaction.user.globalName || interaction.user.username;
|
|
216
217
|
const prompt = `User clicked: ${button.label}`;
|
|
217
|
-
await sendThreadMessage(thread,
|
|
218
|
+
await sendThreadMessage(thread, `${QUEUE_PREFIX}**${username}:** ${button.label}`);
|
|
218
219
|
try {
|
|
219
220
|
await sendClickedActionToModel({
|
|
220
221
|
interaction,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// /add-project command - Create Discord channels for an existing OpenCode project.
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { findRegisteredTextChannelForDirectory, getAllTextChannelDirectories, } from '../database.js';
|
|
5
5
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
6
6
|
import { createProjectChannels } from '../channel-management.js';
|
|
7
7
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
@@ -38,12 +38,9 @@ export async function handleAddProjectCommand({ command, }) {
|
|
|
38
38
|
await command.editReply(`Directory does not exist: ${directory}`);
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
});
|
|
45
|
-
if (existingChannels.length > 0) {
|
|
46
|
-
await command.editReply(`A channel already exists for this directory: <#${existingChannels[0].channel_id}>`);
|
|
41
|
+
const existingChannel = await findRegisteredTextChannelForDirectory(directory);
|
|
42
|
+
if (existingChannel) {
|
|
43
|
+
await command.editReply(`A channel already exists for this directory: <#${existingChannel.channel_id}>`);
|
|
47
44
|
return;
|
|
48
45
|
}
|
|
49
46
|
const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
|
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({
|
|
@@ -513,6 +591,7 @@ async function handleQuickAgentWithPrompt({ command, appId, fallbackAgentName, p
|
|
|
513
591
|
agent: resolvedAgentName,
|
|
514
592
|
appId,
|
|
515
593
|
mode: 'opencode',
|
|
594
|
+
variant,
|
|
516
595
|
});
|
|
517
596
|
}
|
|
518
597
|
else {
|
|
@@ -522,3 +601,58 @@ async function handleQuickAgentWithPrompt({ command, appId, fallbackAgentName, p
|
|
|
522
601
|
});
|
|
523
602
|
}
|
|
524
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
|
+
}
|
|
@@ -7,6 +7,7 @@ import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from '.
|
|
|
7
7
|
import { getOpencodeClient } from '../opencode.js';
|
|
8
8
|
import { DiscordOperationError } from '../errors.js';
|
|
9
9
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
10
|
+
import { QUEUE_PREFIX } from '../message-formatting.js';
|
|
10
11
|
const logger = createLogger(LogPrefix.ASK_QUESTION);
|
|
11
12
|
// Store pending question contexts by hash.
|
|
12
13
|
// No TTL on purpose: a question can only be answered by the user, so expiring it
|
|
@@ -183,7 +184,7 @@ export async function handleAskQuestionSelectMenu(interaction) {
|
|
|
183
184
|
components: [], // Remove the dropdown
|
|
184
185
|
});
|
|
185
186
|
const username = interaction.user.globalName || interaction.user.username;
|
|
186
|
-
await sendThreadMessage(context.thread,
|
|
187
|
+
await sendThreadMessage(context.thread, `${QUEUE_PREFIX}**${username}:** ${answeredText}`);
|
|
187
188
|
// Check if all questions are answered
|
|
188
189
|
if (areAllQuestionsAnswered(context)) {
|
|
189
190
|
// All questions answered - send result back to session
|
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,
|
|
@@ -8,17 +8,23 @@ import { mergeWorktree, listBranchesByLastCommit, validateBranchRef, formatMerge
|
|
|
8
8
|
import { sendThreadMessage, resolveWorkingDirectory, resolveProjectDirectoryFromAutocomplete, } from '../discord-utils.js';
|
|
9
9
|
import { getOrCreateRuntime, } from '../session-handler/thread-session-runtime.js';
|
|
10
10
|
import { RebaseConflictError, DirtyWorktreeError, TargetDirtyWorktreeError, NothingToMergeError, } from '../errors.js';
|
|
11
|
+
import { WORKTREE_PREFIX } from '../message-formatting.js';
|
|
11
12
|
const logger = createLogger(LogPrefix.WORKTREE);
|
|
12
13
|
function quoteShellArg(value) {
|
|
13
14
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
export { WORKTREE_PREFIX };
|
|
17
|
+
function worktreePrefixLength(name) {
|
|
18
|
+
if (name.startsWith(WORKTREE_PREFIX))
|
|
19
|
+
return WORKTREE_PREFIX.length;
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
17
22
|
async function removeWorktreePrefixFromTitle(thread) {
|
|
18
|
-
|
|
23
|
+
const prefixLength = worktreePrefixLength(thread.name);
|
|
24
|
+
if (!prefixLength) {
|
|
19
25
|
return;
|
|
20
26
|
}
|
|
21
|
-
const newName = thread.name.slice(
|
|
27
|
+
const newName = thread.name.slice(prefixLength);
|
|
22
28
|
const timeoutMs = 5000;
|
|
23
29
|
await Promise.race([
|
|
24
30
|
thread.setName(newName).catch((e) => {
|
|
@@ -120,8 +126,9 @@ export async function handleMergeWorktreeCommand({ command, appId, }) {
|
|
|
120
126
|
return;
|
|
121
127
|
}
|
|
122
128
|
if (result instanceof RebaseConflictError) {
|
|
123
|
-
const
|
|
124
|
-
|
|
129
|
+
const prefixLength = worktreePrefixLength(thread.name);
|
|
130
|
+
const mergedThreadName = prefixLength
|
|
131
|
+
? thread.name.slice(prefixLength)
|
|
125
132
|
: thread.name;
|
|
126
133
|
const mergeCommand = [
|
|
127
134
|
'kimaki merge-worktree',
|
|
@@ -138,11 +138,12 @@ export async function showModelVariantPicker({ channel, appId, editReply, }) {
|
|
|
138
138
|
const { providerID, modelID, model: fullModelId } = currentModelInfo;
|
|
139
139
|
const sourceLabel = formatSourceLabel(currentModelInfo);
|
|
140
140
|
const variantLabel = cascadeVariant ? ` (${cascadeVariant})` : '';
|
|
141
|
-
const displayedModelId = resolveDisplayedModelId({
|
|
141
|
+
const displayedModelId = (await resolveDisplayedModelId({
|
|
142
142
|
providers: providersResponse.data.all,
|
|
143
143
|
providerID,
|
|
144
144
|
modelID,
|
|
145
|
-
|
|
145
|
+
sessionID: sessionId,
|
|
146
|
+
})) ?? fullModelId;
|
|
146
147
|
const provider = providersResponse.data.all.find((p) => {
|
|
147
148
|
return p.id === providerID;
|
|
148
149
|
});
|