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.
Files changed (214) hide show
  1. package/dist/agent-model.e2e.test.js +25 -122
  2. package/dist/anthropic-account-identity.js +1 -0
  3. package/dist/anthropic-auth-plugin.js +1 -0
  4. package/dist/anthropic-auth-state.js +1 -0
  5. package/dist/bin.js +73 -19
  6. package/dist/cli-commands/maintenance.js +46 -7
  7. package/dist/cli-commands/send.js +24 -0
  8. package/dist/cli-commands/session.js +183 -8
  9. package/dist/cli-commands/task.js +13 -1
  10. package/dist/cli-parsing.test.js +59 -3
  11. package/dist/cli-runner.js +28 -4
  12. package/dist/cli-runner.test.js +27 -0
  13. package/dist/cli-send-thread.e2e.test.js +1 -1
  14. package/dist/cli.js +32 -0
  15. package/dist/commands/abort.js +5 -1
  16. package/dist/commands/agent.js +6 -8
  17. package/dist/commands/ask-question.js +21 -32
  18. package/dist/commands/ask-question.test.js +43 -1
  19. package/dist/commands/login.js +6 -0
  20. package/dist/commands/merge-worktree.js +36 -10
  21. package/dist/commands/model-variant.js +7 -1
  22. package/dist/commands/model.js +14 -8
  23. package/dist/commands/multioauth.js +1 -0
  24. package/dist/commands/new-worktree.js +170 -102
  25. package/dist/commands/user-command.js +6 -8
  26. package/dist/commands/worktrees.js +5 -13
  27. package/dist/config.js +16 -1
  28. package/dist/database.js +124 -3
  29. package/dist/db.js +22 -0
  30. package/dist/db.test.js +167 -1
  31. package/dist/discord-bot.js +34 -32
  32. package/dist/discord-command-registration.js +9 -1
  33. package/dist/discord-utils.js +13 -5
  34. package/dist/discord-utils.test.js +17 -1
  35. package/dist/errors.js +5 -0
  36. package/dist/exec-async.js +17 -5
  37. package/dist/external-directory-permissions.e2e.test.js +2 -2
  38. package/dist/file-edit-log.js +197 -0
  39. package/dist/file-edit-log.test.js +258 -0
  40. package/dist/format-tables.js +52 -0
  41. package/dist/format-tables.test.js +93 -0
  42. package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
  43. package/dist/gateway-proxy.e2e.test.js +5 -5
  44. package/dist/git-worktree-core.js +182 -17
  45. package/dist/ipc-tools-plugin.js +63 -3
  46. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
  47. package/dist/kimaki-opencode-plugin.js +7 -3
  48. package/dist/kimaki-workspace-adaptor.js +33 -10
  49. package/dist/logger.js +10 -1
  50. package/dist/markdown.js +3 -1
  51. package/dist/markdown.test.js +11 -3
  52. package/dist/message-formatting.js +18 -7
  53. package/dist/message-formatting.test.js +56 -9
  54. package/dist/oauth-rotation-shared.js +10 -0
  55. package/dist/openai-auth-plugin.js +1 -0
  56. package/dist/openai-auth-state.js +1 -0
  57. package/dist/opencode-command.test.js +60 -0
  58. package/dist/opencode.js +53 -12
  59. package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
  60. package/dist/question-text-before-dropdown.e2e.test.js +52 -0
  61. package/dist/queue-advanced-abort.e2e.test.js +1 -1
  62. package/dist/queue-advanced-action-buttons.e2e.test.js +3 -3
  63. package/dist/queue-advanced-e2e-setup.js +78 -5
  64. package/dist/queue-advanced-footer.e2e.test.js +40 -246
  65. package/dist/queue-advanced-model-switch.e2e.test.js +2 -2
  66. package/dist/queue-advanced-permissions-typing.e2e.test.js +7 -9
  67. package/dist/queue-advanced-question.e2e.test.js +31 -26
  68. package/dist/queue-advanced-typing-interrupt.e2e.test.js +2 -2
  69. package/dist/queue-delete-message.e2e.test.js +1 -1
  70. package/dist/queue-drain-after-interactive-ui.e2e.test.js +2 -2
  71. package/dist/queue-interrupt-drain.e2e.test.js +112 -2
  72. package/dist/queue-question-select-drain.e2e.test.js +30 -128
  73. package/dist/runtime-lifecycle.e2e.test.js +10 -37
  74. package/dist/schema.js +34 -0
  75. package/dist/session-handler/event-stream-state.js +267 -50
  76. package/dist/session-handler/event-stream-state.test.js +415 -8
  77. package/dist/session-handler/model-utils.js +159 -5
  78. package/dist/session-handler/model-utils.test.js +345 -0
  79. package/dist/session-handler/thread-session-runtime.js +252 -182
  80. package/dist/session-sleep.test.js +224 -0
  81. package/dist/session-title-rename.test.js +7 -39
  82. package/dist/sleep.e2e.test.js +491 -0
  83. package/dist/startup-time.e2e.test.js +2 -1
  84. package/dist/store.js +3 -0
  85. package/dist/system-message.js +59 -6
  86. package/dist/system-message.test.js +76 -6
  87. package/dist/task-runner.js +94 -4
  88. package/dist/task-schedule.js +56 -0
  89. package/dist/task-schedule.test.js +71 -1
  90. package/dist/thread-message-queue.e2e.test.js +10 -169
  91. package/dist/undo-redo.e2e.test.js +2 -2
  92. package/dist/voice-attachment.js +23 -1
  93. package/dist/voice-message.e2e.test.js +10 -10
  94. package/dist/voice.test.js +55 -29
  95. package/dist/worktree-clone-isolation.e2e.test.js +188 -0
  96. package/dist/worktree-lifecycle.e2e.test.js +29 -10
  97. package/dist/worktrees.js +226 -443
  98. package/dist/worktrees.test.js +435 -58
  99. package/dist/xai-auth-plugin.js +1 -0
  100. package/dist/xai-auth-state.js +1 -0
  101. package/package.json +8 -6
  102. package/skills/errore/SKILL.md +18 -0
  103. package/skills/goke/SKILL.md +150 -10
  104. package/skills/holocron/SKILL.md +37 -0
  105. package/skills/npm-package/SKILL.md +55 -35
  106. package/skills/profano/SKILL.md +17 -2
  107. package/skills/spiceflow/SKILL.md +39 -3
  108. package/skills/strada/SKILL.md +39 -0
  109. package/skills/zele/SKILL.md +19 -0
  110. package/src/agent-model.e2e.test.ts +25 -151
  111. package/src/anthropic-account-identity.ts +1 -0
  112. package/src/anthropic-auth-plugin.ts +1 -0
  113. package/src/anthropic-auth-state.ts +1 -0
  114. package/src/bin.ts +78 -26
  115. package/src/cli-commands/maintenance.ts +59 -13
  116. package/src/cli-commands/send.ts +25 -0
  117. package/src/cli-commands/session.ts +203 -9
  118. package/src/cli-commands/task.ts +13 -1
  119. package/src/cli-parsing.test.ts +69 -3
  120. package/src/cli-runner.test.ts +49 -0
  121. package/src/cli-runner.ts +28 -4
  122. package/src/cli-send-thread.e2e.test.ts +1 -1
  123. package/src/cli.ts +53 -0
  124. package/src/commands/abort.ts +6 -1
  125. package/src/commands/agent.ts +6 -6
  126. package/src/commands/ask-question.test.ts +48 -1
  127. package/src/commands/ask-question.ts +21 -32
  128. package/src/commands/login.ts +6 -0
  129. package/src/commands/merge-worktree.ts +43 -12
  130. package/src/commands/model-variant.ts +8 -1
  131. package/src/commands/model.ts +18 -8
  132. package/src/commands/multioauth.ts +1 -0
  133. package/src/commands/new-worktree.ts +138 -55
  134. package/src/commands/user-command.ts +6 -6
  135. package/src/commands/worktrees.ts +5 -12
  136. package/src/config.ts +18 -1
  137. package/src/database.ts +195 -3
  138. package/src/db.test.ts +188 -0
  139. package/src/db.ts +26 -0
  140. package/src/discord-bot.ts +35 -29
  141. package/src/discord-command-registration.ts +15 -1
  142. package/src/discord-utils.test.ts +19 -0
  143. package/src/discord-utils.ts +21 -6
  144. package/src/errors.ts +5 -0
  145. package/src/exec-async.ts +20 -11
  146. package/src/external-directory-permissions.e2e.test.ts +2 -2
  147. package/src/file-edit-log.test.ts +287 -0
  148. package/src/file-edit-log.ts +283 -0
  149. package/src/format-tables.test.ts +101 -0
  150. package/src/format-tables.ts +75 -0
  151. package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
  152. package/src/gateway-proxy.e2e.test.ts +5 -5
  153. package/src/git-worktree-core.ts +247 -20
  154. package/src/ipc-tools-plugin.ts +75 -5
  155. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
  156. package/src/kimaki-opencode-plugin.ts +7 -3
  157. package/src/kimaki-workspace-adaptor.ts +40 -11
  158. package/src/logger.ts +13 -4
  159. package/src/markdown.test.ts +13 -3
  160. package/src/markdown.ts +4 -1
  161. package/src/message-formatting.test.ts +86 -12
  162. package/src/message-formatting.ts +19 -7
  163. package/src/oauth-rotation-shared.ts +10 -0
  164. package/src/openai-auth-plugin.ts +1 -0
  165. package/src/openai-auth-state.ts +1 -0
  166. package/src/opencode-command.test.ts +81 -0
  167. package/src/opencode.ts +71 -11
  168. package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
  169. package/src/question-text-before-dropdown.e2e.test.ts +67 -0
  170. package/src/queue-advanced-abort.e2e.test.ts +1 -1
  171. package/src/queue-advanced-action-buttons.e2e.test.ts +3 -3
  172. package/src/queue-advanced-e2e-setup.ts +83 -4
  173. package/src/queue-advanced-footer.e2e.test.ts +47 -294
  174. package/src/queue-advanced-model-switch.e2e.test.ts +2 -2
  175. package/src/queue-advanced-permissions-typing.e2e.test.ts +7 -9
  176. package/src/queue-advanced-question.e2e.test.ts +35 -29
  177. package/src/queue-advanced-typing-interrupt.e2e.test.ts +2 -2
  178. package/src/queue-delete-message.e2e.test.ts +1 -1
  179. package/src/queue-drain-after-interactive-ui.e2e.test.ts +2 -2
  180. package/src/queue-interrupt-drain.e2e.test.ts +123 -2
  181. package/src/queue-question-select-drain.e2e.test.ts +32 -157
  182. package/src/runtime-lifecycle.e2e.test.ts +10 -41
  183. package/src/schema.sql +14 -1
  184. package/src/schema.ts +36 -0
  185. package/src/session-handler/event-stream-state.test.ts +437 -7
  186. package/src/session-handler/event-stream-state.ts +389 -53
  187. package/src/session-handler/model-utils.test.ts +415 -0
  188. package/src/session-handler/model-utils.ts +249 -5
  189. package/src/session-handler/thread-session-runtime.ts +300 -215
  190. package/src/session-sleep.test.ts +294 -0
  191. package/src/session-title-rename.test.ts +9 -48
  192. package/src/sleep.e2e.test.ts +562 -0
  193. package/src/startup-time.e2e.test.ts +5 -1
  194. package/src/store.ts +21 -0
  195. package/src/system-message.test.ts +86 -6
  196. package/src/system-message.ts +66 -7
  197. package/src/task-runner.ts +136 -2
  198. package/src/task-schedule.test.ts +76 -0
  199. package/src/task-schedule.ts +95 -0
  200. package/src/thread-message-queue.e2e.test.ts +10 -202
  201. package/src/undo-redo.e2e.test.ts +2 -2
  202. package/src/voice-attachment.ts +29 -1
  203. package/src/voice-message.e2e.test.ts +10 -10
  204. package/src/voice.test.ts +63 -29
  205. package/src/worktree-clone-isolation.e2e.test.ts +213 -0
  206. package/src/worktree-lifecycle.e2e.test.ts +29 -9
  207. package/src/worktrees.test.ts +505 -64
  208. package/src/worktrees.ts +257 -603
  209. package/src/xai-auth-plugin.ts +1 -0
  210. package/src/xai-auth-state.ts +1 -0
  211. package/dist/message-finish-field.e2e.test.js +0 -169
  212. package/dist/subagent-rate-limit-plugin.js +0 -184
  213. package/src/message-finish-field.e2e.test.ts +0 -196
  214. 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 prevents unbounded growth if user never answers a question.
12
- const QUESTION_CONTEXT_TTL_MS = 10 * 60 * 1000;
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 TTL can retry).
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 TTL can still fire.
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,
@@ -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 - Merge worktree commits into default branch.
2
- // Pipeline: rebase worktree commits onto target -> local fast-forward push.
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(`Merge failed: ${result.message}`);
119
+ await command.editReply(formatMergeWorktreeError(result));
110
120
  return;
111
121
  }
112
122
  if (result instanceof RebaseConflictError) {
113
- await command.editReply('Rebase conflict detected. Asking the model to resolve...');
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
- '9. Once the rebase is fully complete, tell me so I can run `/merge-worktree` again',
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
- await command.editReply(`Merge failed: ${result.message}`);
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
- await command.editReply(`Merged \`${result.branchName}\` into \`${result.defaultBranch}\` @ ${result.shortSha} (${result.commitCount} commit${result.commitCount === 1 ? '' : 's'})\nWorktree now at detached HEAD.`);
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:** \`${fullModelId}\`${variantLabel} — ${sourceLabel}`;
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.`,
@@ -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):** \`${currentModelInfo.model}\``;
335
+ return `**Current (this thread):** \`${displayed}\``;
328
336
  case 'agent':
329
- return `**Current (agent "${currentModelInfo.agentName}"):** \`${currentModelInfo.model}\``;
337
+ return `**Current (agent "${currentModelInfo.agentName}"):** \`${displayed}\``;
330
338
  case 'channel':
331
- return `**Current (channel override):** \`${currentModelInfo.model}\``;
339
+ return `**Current (channel override):** \`${displayed}\``;
332
340
  case 'global':
333
- return `**Current (global default):** \`${currentModelInfo.model}\``;
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):** \`${currentModelInfo.model}\``;
338
- case 'none':
339
- return '**Current:** none';
345
+ return `**Current (opencode default):** \`${displayed}\``;
340
346
  }
341
347
  })();
342
348
  const variantText = (() => {
@@ -1,3 +1,4 @@
1
+ // LEGACY rotation, superseded by @subrouter/opencode. See oauth-rotation-shared.ts.
1
2
  /**
2
3
  * CLI commands for multi-provider OAuth account management.
3
4
  * Mounted via goke .use() in cli.ts under the `multioauth` namespace.