kimaki 0.26.0 → 0.27.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 +25 -122
- package/dist/anthropic-account-identity.js +1 -0
- package/dist/anthropic-auth-plugin.js +1 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/send.js +24 -0
- package/dist/cli-commands/session.js +183 -8
- package/dist/cli-commands/task.js +13 -1
- package/dist/cli-parsing.test.js +59 -3
- package/dist/cli-runner.js +28 -4
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +1 -1
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/agent.js +6 -8
- package/dist/commands/ask-question.js +21 -32
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +36 -10
- package/dist/commands/model-variant.js +7 -1
- package/dist/commands/model.js +14 -8
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/database.js +124 -3
- package/dist/db.js +22 -0
- package/dist/db.test.js +167 -1
- package/dist/discord-bot.js +34 -32
- package/dist/discord-command-registration.js +9 -1
- package/dist/discord-utils.js +13 -5
- package/dist/discord-utils.test.js +17 -1
- package/dist/errors.js +5 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +2 -2
- 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 +5 -5
- package/dist/git-worktree-core.js +182 -17
- package/dist/ipc-tools-plugin.js +63 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +7 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +10 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +18 -7
- package/dist/message-formatting.test.js +56 -9
- 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.test.js +60 -0
- package/dist/opencode.js +53 -12
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +1 -1
- package/dist/queue-advanced-action-buttons.e2e.test.js +3 -3
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +40 -246
- package/dist/queue-advanced-model-switch.e2e.test.js +2 -2
- package/dist/queue-advanced-permissions-typing.e2e.test.js +7 -9
- package/dist/queue-advanced-question.e2e.test.js +31 -26
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +2 -2
- package/dist/queue-delete-message.e2e.test.js +1 -1
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +2 -2
- package/dist/queue-interrupt-drain.e2e.test.js +112 -2
- package/dist/queue-question-select-drain.e2e.test.js +30 -128
- package/dist/runtime-lifecycle.e2e.test.js +10 -37
- package/dist/schema.js +34 -0
- package/dist/session-handler/event-stream-state.js +267 -50
- package/dist/session-handler/event-stream-state.test.js +415 -8
- package/dist/session-handler/model-utils.js +159 -5
- package/dist/session-handler/model-utils.test.js +345 -0
- package/dist/session-handler/thread-session-runtime.js +252 -182
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +7 -39
- package/dist/sleep.e2e.test.js +491 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +59 -6
- package/dist/system-message.test.js +76 -6
- package/dist/task-runner.js +94 -4
- package/dist/task-schedule.js +56 -0
- package/dist/task-schedule.test.js +71 -1
- package/dist/thread-message-queue.e2e.test.js +10 -169
- package/dist/undo-redo.e2e.test.js +2 -2
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-message.e2e.test.js +10 -10
- package/dist/voice.test.js +55 -29
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +29 -10
- 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/errore/SKILL.md +18 -0
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +37 -0
- 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 +25 -151
- package/src/anthropic-account-identity.ts +1 -0
- package/src/anthropic-auth-plugin.ts +1 -0
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/send.ts +25 -0
- package/src/cli-commands/session.ts +203 -9
- package/src/cli-commands/task.ts +13 -1
- package/src/cli-parsing.test.ts +69 -3
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +28 -4
- package/src/cli-send-thread.e2e.test.ts +1 -1
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/agent.ts +6 -6
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +21 -32
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +43 -12
- package/src/commands/model-variant.ts +8 -1
- package/src/commands/model.ts +18 -8
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/database.ts +195 -3
- package/src/db.test.ts +188 -0
- package/src/db.ts +26 -0
- package/src/discord-bot.ts +35 -29
- package/src/discord-command-registration.ts +15 -1
- package/src/discord-utils.test.ts +19 -0
- package/src/discord-utils.ts +21 -6
- package/src/errors.ts +5 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +2 -2
- 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 +5 -5
- package/src/git-worktree-core.ts +247 -20
- package/src/ipc-tools-plugin.ts +75 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +7 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +13 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +86 -12
- package/src/message-formatting.ts +19 -7
- 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 +81 -0
- package/src/opencode.ts +71 -11
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +1 -1
- package/src/queue-advanced-action-buttons.e2e.test.ts +3 -3
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +47 -294
- package/src/queue-advanced-model-switch.e2e.test.ts +2 -2
- package/src/queue-advanced-permissions-typing.e2e.test.ts +7 -9
- package/src/queue-advanced-question.e2e.test.ts +35 -29
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +2 -2
- package/src/queue-delete-message.e2e.test.ts +1 -1
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +2 -2
- package/src/queue-interrupt-drain.e2e.test.ts +123 -2
- package/src/queue-question-select-drain.e2e.test.ts +32 -157
- package/src/runtime-lifecycle.e2e.test.ts +10 -41
- package/src/schema.sql +14 -1
- package/src/schema.ts +36 -0
- package/src/session-handler/event-stream-state.test.ts +437 -7
- package/src/session-handler/event-stream-state.ts +389 -53
- package/src/session-handler/model-utils.test.ts +415 -0
- package/src/session-handler/model-utils.ts +249 -5
- package/src/session-handler/thread-session-runtime.ts +300 -215
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +9 -48
- package/src/sleep.e2e.test.ts +562 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +21 -0
- package/src/system-message.test.ts +86 -6
- package/src/system-message.ts +66 -7
- package/src/task-runner.ts +136 -2
- package/src/task-schedule.test.ts +76 -0
- package/src/task-schedule.ts +95 -0
- package/src/thread-message-queue.e2e.test.ts +10 -202
- package/src/undo-redo.e2e.test.ts +2 -2
- package/src/voice-attachment.ts +29 -1
- package/src/voice-message.e2e.test.ts +10 -10
- package/src/voice.test.ts +63 -29
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +29 -9
- 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
|
@@ -5,11 +5,13 @@ 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';
|
|
9
10
|
const logger = createLogger(LogPrefix.ASK_QUESTION);
|
|
10
11
|
// Store pending question contexts by hash.
|
|
11
|
-
// TTL
|
|
12
|
-
|
|
12
|
+
// No TTL on purpose: a question can only be answered by the user, so expiring it
|
|
13
|
+
// could only abort the run. Cleanup happens on answer, on a new user message, on
|
|
14
|
+
// abort (abortActiveRunInternal), and on thread dispose.
|
|
13
15
|
export const pendingQuestionContexts = new Map();
|
|
14
16
|
export function areAllQuestionsAnswered({ totalQuestions, answers, }) {
|
|
15
17
|
for (let i = 0; i < totalQuestions; i++) {
|
|
@@ -75,32 +77,6 @@ export async function showAskUserQuestionDropdowns({ thread, sessionId, director
|
|
|
75
77
|
contextHash,
|
|
76
78
|
};
|
|
77
79
|
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
80
|
// Send one message per question with its dropdown directly underneath
|
|
105
81
|
for (let i = 0; i < input.questions.length; i++) {
|
|
106
82
|
const q = input.questions[i];
|
|
@@ -129,11 +105,24 @@ export async function showAskUserQuestionDropdowns({ thread, sessionId, director
|
|
|
129
105
|
selectMenu.setMaxValues(options.length);
|
|
130
106
|
}
|
|
131
107
|
const actionRow = new ActionRowBuilder().addComponents(selectMenu);
|
|
132
|
-
await thread.send({
|
|
108
|
+
const sendResult = await thread.send({
|
|
133
109
|
content: `**${(q.header || '').slice(0, 200)}**\n${q.question.slice(0, 1700)}`,
|
|
134
110
|
components: [actionRow],
|
|
135
111
|
flags: silent ? SILENT_MESSAGE_FLAGS : NOTIFY_MESSAGE_FLAGS,
|
|
136
|
-
});
|
|
112
|
+
}).catch((e) => new DiscordOperationError({ operation: 'sendQuestionDropdown', cause: e }));
|
|
113
|
+
// Without a visible dropdown the user can never answer, and there is no TTL
|
|
114
|
+
// to rescue the run, so drop the context and abort so OpenCode unblocks.
|
|
115
|
+
if (sendResult instanceof Error) {
|
|
116
|
+
logger.error('Failed to send question dropdown:', sendResult);
|
|
117
|
+
deletePendingQuestionContextsForRequest({ threadId: thread.id, requestId });
|
|
118
|
+
const client = getOpencodeClient(directory);
|
|
119
|
+
if (client) {
|
|
120
|
+
await client.session.abort({ sessionID: sessionId }).catch((error) => {
|
|
121
|
+
logger.error('Failed to abort session after question send failure:', error);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
137
126
|
}
|
|
138
127
|
logger.log(`Showed ${input.questions.length} question dropdown(s) for session ${sessionId}`);
|
|
139
128
|
}
|
|
@@ -276,7 +265,7 @@ export function parseAskUserQuestionTool(part) {
|
|
|
276
265
|
* the tool answer so the model sees the user's response. The caller should
|
|
277
266
|
* NOT also enqueue the message as a new prompt.
|
|
278
267
|
* Returns 'replied' on success, 'reply-failed' if the reply call fails
|
|
279
|
-
* (context kept pending so
|
|
268
|
+
* (context kept pending so the user can retry).
|
|
280
269
|
*/
|
|
281
270
|
export async function cancelPendingQuestion(threadId, userMessage) {
|
|
282
271
|
// Find pending question for this thread
|
|
@@ -320,7 +309,7 @@ export async function cancelPendingQuestion(threadId, userMessage) {
|
|
|
320
309
|
}
|
|
321
310
|
catch (error) {
|
|
322
311
|
logger.error('Failed to answer question:', error);
|
|
323
|
-
// Keep context pending so
|
|
312
|
+
// Keep context pending so the user can retry.
|
|
324
313
|
// Caller should not consume the user message since reply failed.
|
|
325
314
|
return 'reply-failed';
|
|
326
315
|
}
|
|
@@ -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/login.js
CHANGED
|
@@ -12,6 +12,7 @@ import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectM
|
|
|
12
12
|
import crypto from 'node:crypto';
|
|
13
13
|
import { initializeOpencodeForDirectory, getOpencodeServerPort, getOpencodeServerAuthHeaders, } from '../opencode.js';
|
|
14
14
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
15
|
+
import { clearModelListCache } from '../session-handler/model-utils.js';
|
|
15
16
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
16
17
|
import { buildPaginatedOptions, parsePaginationValue } from './paginated-select.js';
|
|
17
18
|
const loginLogger = createLogger(LogPrefix.LOGIN);
|
|
@@ -31,7 +32,9 @@ function createContextHash(context) {
|
|
|
31
32
|
// ── Provider popularity order ───────────────────────────────────
|
|
32
33
|
// Discord select menus cap at 25 options, so we show popular ones first.
|
|
33
34
|
// IDs sourced from opencode's provider.list() API (scripts/list-providers.ts).
|
|
35
|
+
// `subrouter` leads: it pools every subscription and cycles across them.
|
|
34
36
|
const PROVIDER_POPULARITY_ORDER = [
|
|
37
|
+
'subrouter',
|
|
35
38
|
'anthropic',
|
|
36
39
|
'openai',
|
|
37
40
|
'google',
|
|
@@ -691,6 +694,7 @@ export async function handleOAuthCodeModalSubmit(interaction) {
|
|
|
691
694
|
return;
|
|
692
695
|
}
|
|
693
696
|
await getClient().instance.dispose({ directory: ctx.dir });
|
|
697
|
+
clearModelListCache();
|
|
694
698
|
pendingLoginContexts.delete(hash);
|
|
695
699
|
await interaction.editReply({
|
|
696
700
|
content: `✅ **Successfully authenticated with ${ctx.providerName}!**\n\nYou can now use models from this provider.`,
|
|
@@ -736,6 +740,7 @@ export async function handleApiKeyModalSubmit(interaction) {
|
|
|
736
740
|
});
|
|
737
741
|
// Dispose to refresh provider state so new credentials are recognized
|
|
738
742
|
await getClient().instance.dispose({ directory: ctx.dir });
|
|
743
|
+
clearModelListCache();
|
|
739
744
|
await interaction.editReply({
|
|
740
745
|
content: `✅ **Successfully authenticated with ${ctx.providerName}!**\n\nYou can now use models from this provider.`,
|
|
741
746
|
});
|
|
@@ -870,6 +875,7 @@ async function startOAuthFlow(interaction, ctx, hash) {
|
|
|
870
875
|
return;
|
|
871
876
|
}
|
|
872
877
|
await getClient().instance.dispose({ directory: ctx.dir });
|
|
878
|
+
clearModelListCache();
|
|
873
879
|
pendingLoginContexts.delete(hash);
|
|
874
880
|
await interaction.editReply({
|
|
875
881
|
content: `✅ **Successfully authenticated with ${ctx.providerName}!**\n\nYou can now use models from this provider.`,
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
// /merge-worktree command -
|
|
2
|
-
//
|
|
3
|
-
// Preserves all commits (no squash). On rebase conflicts, asks the AI model
|
|
4
|
-
// in the thread to resolve them.
|
|
1
|
+
// /merge-worktree command - Rebase or squash worktree commits into a target branch.
|
|
2
|
+
// On rebase conflicts, asks the AI model in the thread to resolve them.
|
|
5
3
|
import {} from 'discord.js';
|
|
6
4
|
import { getThreadWorktreeOrWorkspace, getThreadSession, getChannelDirectory, } from '../database.js';
|
|
7
5
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
8
6
|
import { notifyError } from '../sentry.js';
|
|
9
|
-
import { mergeWorktree, listBranchesByLastCommit, validateBranchRef } from '../worktrees.js';
|
|
7
|
+
import { mergeWorktree, listBranchesByLastCommit, validateBranchRef, formatMergeWorktreeError, } from '../worktrees.js';
|
|
10
8
|
import { sendThreadMessage, resolveWorkingDirectory, resolveProjectDirectoryFromAutocomplete, } from '../discord-utils.js';
|
|
11
9
|
import { getOrCreateRuntime, } from '../session-handler/thread-session-runtime.js';
|
|
12
10
|
import { RebaseConflictError, DirtyWorktreeError, TargetDirtyWorktreeError, NothingToMergeError, } from '../errors.js';
|
|
13
11
|
const logger = createLogger(LogPrefix.WORKTREE);
|
|
12
|
+
function quoteShellArg(value) {
|
|
13
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
14
|
+
}
|
|
14
15
|
/** Worktree thread title prefix - indicates unmerged worktree */
|
|
15
16
|
export const WORKTREE_PREFIX = '⬦ ';
|
|
16
17
|
async function removeWorktreePrefixFromTitle(thread) {
|
|
@@ -73,6 +74,14 @@ export async function handleMergeWorktreeCommand({ command, appId, }) {
|
|
|
73
74
|
await command.editReply(`Worktree is not ready (status: ${info.status})${info.error_message ? `: ${info.error_message}` : ''}`);
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
77
|
+
const strategyOption = command.options.getString('strategy') || 'rebase';
|
|
78
|
+
if (strategyOption !== 'rebase' && strategyOption !== 'squash') {
|
|
79
|
+
await command.editReply(`Invalid merge strategy: \`${strategyOption}\``);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const strategyName = strategyOption === 'squash'
|
|
83
|
+
? 'Squash into one commit'
|
|
84
|
+
: 'Keep commits (rebase)';
|
|
76
85
|
const rawTargetBranch = command.options.getString('target-branch') || undefined;
|
|
77
86
|
let targetBranch = rawTargetBranch;
|
|
78
87
|
if (targetBranch) {
|
|
@@ -91,6 +100,7 @@ export async function handleMergeWorktreeCommand({ command, appId, }) {
|
|
|
91
100
|
mainRepoDir: info.project_directory,
|
|
92
101
|
worktreeName: info.workspace_name,
|
|
93
102
|
targetBranch,
|
|
103
|
+
strategy: strategyOption,
|
|
94
104
|
onProgress: (msg) => {
|
|
95
105
|
logger.log(`[merge] ${msg}`);
|
|
96
106
|
},
|
|
@@ -106,11 +116,21 @@ export async function handleMergeWorktreeCommand({ command, appId, }) {
|
|
|
106
116
|
}
|
|
107
117
|
if (result instanceof NothingToMergeError) {
|
|
108
118
|
void removeWorktreePrefixFromTitle(thread);
|
|
109
|
-
await command.editReply(
|
|
119
|
+
await command.editReply(formatMergeWorktreeError(result));
|
|
110
120
|
return;
|
|
111
121
|
}
|
|
112
122
|
if (result instanceof RebaseConflictError) {
|
|
113
|
-
|
|
123
|
+
const mergedThreadName = thread.name.startsWith(WORKTREE_PREFIX)
|
|
124
|
+
? thread.name.slice(WORKTREE_PREFIX.length)
|
|
125
|
+
: thread.name;
|
|
126
|
+
const mergeCommand = [
|
|
127
|
+
'kimaki merge-worktree',
|
|
128
|
+
`--strategy ${strategyOption}`,
|
|
129
|
+
`--target-branch ${quoteShellArg(String(result.target))}`,
|
|
130
|
+
`--thread ${quoteShellArg(thread.id)}`,
|
|
131
|
+
`--thread-name ${quoteShellArg(mergedThreadName)}`,
|
|
132
|
+
].join(' ');
|
|
133
|
+
await command.editReply(`Rebase conflict detected. Asking the model to resolve it and retry the merge with **${strategyName}**.`);
|
|
114
134
|
await sendPromptToModel({
|
|
115
135
|
prompt: [
|
|
116
136
|
`A rebase conflict occurred while merging this worktree into \`${result.target}\`.`,
|
|
@@ -124,7 +144,8 @@ export async function handleMergeWorktreeCommand({ command, appId, }) {
|
|
|
124
144
|
'6. Stage resolved files with `git add`',
|
|
125
145
|
'7. Continue the rebase with `git rebase --continue`',
|
|
126
146
|
'8. If git reports more conflicts, repeat steps 1-7 until the rebase finishes (no more rebase in progress, `git status` is clean)',
|
|
127
|
-
|
|
147
|
+
`9. Once the rebase is fully complete, run \`${mergeCommand}\` yourself`,
|
|
148
|
+
'10. If that command fails because the target changed or is not ready, stop and report the failure. The user can run it again later.',
|
|
128
149
|
].join('\n'),
|
|
129
150
|
thread,
|
|
130
151
|
projectDirectory: info.project_directory,
|
|
@@ -133,11 +154,16 @@ export async function handleMergeWorktreeCommand({ command, appId, }) {
|
|
|
133
154
|
});
|
|
134
155
|
return;
|
|
135
156
|
}
|
|
136
|
-
|
|
157
|
+
const formatted = formatMergeWorktreeError(result);
|
|
158
|
+
logger.error(formatted);
|
|
159
|
+
await command.editReply(formatted);
|
|
137
160
|
return;
|
|
138
161
|
}
|
|
139
162
|
void removeWorktreePrefixFromTitle(thread);
|
|
140
|
-
|
|
163
|
+
const mergeSummary = strategyOption === 'squash'
|
|
164
|
+
? `Squashed \`${result.branchName}\` into \`${result.defaultBranch}\` @ ${result.shortSha} (${result.commitCount} source commit${result.commitCount === 1 ? '' : 's'} → 1)`
|
|
165
|
+
: `Merged \`${result.branchName}\` into \`${result.defaultBranch}\` @ ${result.shortSha} (${result.commitCount} commit${result.commitCount === 1 ? '' : 's'})`;
|
|
166
|
+
await command.editReply(`${mergeSummary}\nWorktree now at detached HEAD.`);
|
|
141
167
|
}
|
|
142
168
|
/**
|
|
143
169
|
* Autocomplete handler for /merge-worktree target-branch option.
|
|
@@ -13,6 +13,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
|
13
13
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
14
14
|
import { getCurrentModelInfo, ensureSessionPreferencesSnapshot, applyToCurrentSession, } from './model.js';
|
|
15
15
|
import { getThinkingValuesForModel } from '../thinking-utils.js';
|
|
16
|
+
import { resolveDisplayedModelId } from '../session-handler/model-utils.js';
|
|
16
17
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
17
18
|
const logger = createLogger(LogPrefix.MODEL);
|
|
18
19
|
const pendingVariantContexts = new Map();
|
|
@@ -137,6 +138,11 @@ export async function showModelVariantPicker({ channel, appId, editReply, }) {
|
|
|
137
138
|
const { providerID, modelID, model: fullModelId } = currentModelInfo;
|
|
138
139
|
const sourceLabel = formatSourceLabel(currentModelInfo);
|
|
139
140
|
const variantLabel = cascadeVariant ? ` (${cascadeVariant})` : '';
|
|
141
|
+
const displayedModelId = resolveDisplayedModelId({
|
|
142
|
+
providers: providersResponse.data.all,
|
|
143
|
+
providerID,
|
|
144
|
+
modelID,
|
|
145
|
+
}) ?? fullModelId;
|
|
140
146
|
const provider = providersResponse.data.all.find((p) => {
|
|
141
147
|
return p.id === providerID;
|
|
142
148
|
});
|
|
@@ -146,7 +152,7 @@ export async function showModelVariantPicker({ channel, appId, editReply, }) {
|
|
|
146
152
|
providerId: providerID,
|
|
147
153
|
modelId: modelID,
|
|
148
154
|
});
|
|
149
|
-
const statusText = `**Current model:** \`${
|
|
155
|
+
const statusText = `**Current model:** \`${displayedModelId}\`${variantLabel} — ${sourceLabel}`;
|
|
150
156
|
if (variants.length === 0) {
|
|
151
157
|
await editReply({
|
|
152
158
|
content: `${statusText}\nThis model doesn't support thinking level variants.`,
|
package/dist/commands/model.js
CHANGED
|
@@ -4,7 +4,7 @@ import crypto from 'node:crypto';
|
|
|
4
4
|
import { setChannelModel, setSessionModel, setSessionAgent, getChannelModel, getSessionModel, getSessionAgent, getChannelAgent, getThreadSession, getGlobalModel, setGlobalModel, getVariantCascade, } from '../database.js';
|
|
5
5
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
6
6
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
7
|
-
import { getDefaultModel } from '../session-handler/model-utils.js';
|
|
7
|
+
import { getDefaultModel, resolveDisplayedModelId, } from '../session-handler/model-utils.js';
|
|
8
8
|
import { getRuntime } from '../session-handler/thread-session-runtime.js';
|
|
9
9
|
import { getThinkingValuesForModel } from '../thinking-utils.js';
|
|
10
10
|
import { buildHtmlActionCustomId, cancelHtmlActionsForOwner, registerHtmlAction, } from '../html-actions.js';
|
|
@@ -322,21 +322,27 @@ export async function handleModelCommand({ interaction, appId, }) {
|
|
|
322
322
|
return;
|
|
323
323
|
}
|
|
324
324
|
const currentModelText = (() => {
|
|
325
|
+
if (currentModelInfo.type === 'none') {
|
|
326
|
+
return '**Current:** none';
|
|
327
|
+
}
|
|
328
|
+
const displayed = resolveDisplayedModelId({
|
|
329
|
+
providers: allProviders,
|
|
330
|
+
providerID: currentModelInfo.providerID,
|
|
331
|
+
modelID: currentModelInfo.modelID,
|
|
332
|
+
}) ?? currentModelInfo.model;
|
|
325
333
|
switch (currentModelInfo.type) {
|
|
326
334
|
case 'session':
|
|
327
|
-
return `**Current (this thread):** \`${
|
|
335
|
+
return `**Current (this thread):** \`${displayed}\``;
|
|
328
336
|
case 'agent':
|
|
329
|
-
return `**Current (agent "${currentModelInfo.agentName}"):** \`${
|
|
337
|
+
return `**Current (agent "${currentModelInfo.agentName}"):** \`${displayed}\``;
|
|
330
338
|
case 'channel':
|
|
331
|
-
return `**Current (channel override):** \`${
|
|
339
|
+
return `**Current (channel override):** \`${displayed}\``;
|
|
332
340
|
case 'global':
|
|
333
|
-
return `**Current (global default):** \`${
|
|
341
|
+
return `**Current (global default):** \`${displayed}\``;
|
|
334
342
|
case 'opencode-config':
|
|
335
343
|
case 'opencode-recent':
|
|
336
344
|
case 'opencode-provider-default':
|
|
337
|
-
return `**Current (opencode default):** \`${
|
|
338
|
-
case 'none':
|
|
339
|
-
return '**Current:** none';
|
|
345
|
+
return `**Current (opencode default):** \`${displayed}\``;
|
|
340
346
|
}
|
|
341
347
|
})();
|
|
342
348
|
const variantText = (() => {
|