kimaki 0.4.45 → 0.4.46

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.
@@ -63,6 +63,8 @@ export async function handleCreateNewProjectCommand({ command, appId, }) {
63
63
  autoArchiveDuration: 1440,
64
64
  reason: 'New project session',
65
65
  });
66
+ // Add user to thread so it appears in their sidebar
67
+ await thread.members.add(command.user.id);
66
68
  await handleOpencodeSession({
67
69
  prompt: 'The project was just initialized. Say hi and ask what the user wants to build.',
68
70
  thread,
@@ -162,6 +162,8 @@ export async function handleForkSelectMenu(interaction) {
162
162
  autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
163
163
  reason: `Forked from session ${sessionId}`,
164
164
  });
165
+ // Add user to thread so it appears in their sidebar
166
+ await thread.members.add(interaction.user.id);
165
167
  getDatabase()
166
168
  .prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
167
169
  .run(thread.id, forkedSession.id);
@@ -51,6 +51,8 @@ export async function handleResumeCommand({ command, appId }) {
51
51
  autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
52
52
  reason: `Resuming session ${sessionId}`,
53
53
  });
54
+ // Add user to thread so it appears in their sidebar
55
+ await thread.members.add(command.user.id);
54
56
  getDatabase()
55
57
  .prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
56
58
  .run(thread.id, sessionId);
@@ -58,6 +58,8 @@ export async function handleSessionCommand({ command, appId }) {
58
58
  autoArchiveDuration: 1440,
59
59
  reason: 'OpenCode session',
60
60
  });
61
+ // Add user to thread so it appears in their sidebar
62
+ await thread.members.add(command.user.id);
61
63
  await command.editReply(`Created new session in ${thread.toString()}`);
62
64
  await handleOpencodeSession({
63
65
  prompt: fullPrompt,
@@ -105,6 +105,8 @@ export const handleUserCommand = async ({ command, appId }) => {
105
105
  autoArchiveDuration: 1440,
106
106
  reason: `OpenCode command: ${commandName}`,
107
107
  });
108
+ // Add user to thread so it appears in their sidebar
109
+ await newThread.members.add(command.user.id);
108
110
  await command.editReply(`Started /${commandName} in ${newThread.toString()}`);
109
111
  await handleOpencodeSession({
110
112
  prompt: '', // Not used when command is set
@@ -167,6 +167,8 @@ export async function handleNewWorktreeCommand({ command, appId, }) {
167
167
  autoArchiveDuration: 1440,
168
168
  reason: 'Worktree session',
169
169
  });
170
+ // Add user to thread so it appears in their sidebar
171
+ await thread.members.add(command.user.id);
170
172
  return { thread, starterMessage };
171
173
  },
172
174
  catch: (e) => new WorktreeError('Failed to create thread', { cause: e }),
@@ -309,6 +309,8 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
309
309
  autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
310
310
  reason: 'Start Claude session',
311
311
  });
312
+ // Add user to thread so it appears in their sidebar
313
+ await thread.members.add(message.author.id);
312
314
  discordLogger.log(`Created thread "${thread.name}" (${thread.id})`);
313
315
  // Create worktree if worktrees are enabled (CLI flag OR channel setting)
314
316
  let sessionDirectory = projectDirectory;
@@ -29,6 +29,7 @@ function buildPermissionDedupeKey({ permission, directory, }) {
29
29
  // Queue of messages waiting to be sent after current response finishes
30
30
  // Key is threadId, value is array of queued messages
31
31
  export const messageQueue = new Map();
32
+ const activeEventHandlers = new Map();
32
33
  export function addToQueue({ threadId, message, }) {
33
34
  const queue = messageQueue.get(threadId) || [];
34
35
  queue.push(message);
@@ -176,6 +177,15 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
176
177
  if (existingController) {
177
178
  voiceLogger.log(`[ABORT] Cancelling existing request for session: ${session.id}`);
178
179
  existingController.abort(new Error('New request started'));
180
+ const abortResult = await errore.tryAsync(() => {
181
+ return getClient().session.abort({
182
+ path: { id: session.id },
183
+ query: { directory: sdkDirectory },
184
+ });
185
+ });
186
+ if (abortResult instanceof Error) {
187
+ sessionLogger.log(`[ABORT] Server abort failed (may be already done):`, abortResult);
188
+ }
179
189
  }
180
190
  // Auto-reject ALL pending permissions for this thread
181
191
  const threadPermissions = pendingPermissions.get(thread.id);
@@ -230,6 +240,16 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
230
240
  sessionLogger.log(`[DEBOUNCE] Aborted before subscribe, exiting`);
231
241
  return;
232
242
  }
243
+ const previousHandler = activeEventHandlers.get(thread.id);
244
+ if (previousHandler) {
245
+ sessionLogger.log(`[EVENT] Waiting for previous handler to finish`);
246
+ await Promise.race([
247
+ previousHandler,
248
+ new Promise((resolve) => {
249
+ setTimeout(resolve, 1000);
250
+ }),
251
+ ]);
252
+ }
233
253
  // Use v2 client for event subscription (has proper types for question.asked events)
234
254
  const clientV2 = getOpencodeClientV2(directory);
235
255
  if (!clientV2) {
@@ -254,6 +274,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
254
274
  let lastDisplayedContextPercentage = 0;
255
275
  let modelContextLimit;
256
276
  let assistantMessageId;
277
+ let handlerPromise = null;
257
278
  let typingInterval = null;
258
279
  function startTyping() {
259
280
  if (abortController.signal.aborted) {
@@ -509,6 +530,10 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
509
530
  }
510
531
  };
511
532
  const handleSubtaskPart = async (part, subtaskInfo) => {
533
+ // In text-only mode, skip all subtask output (they're tool-related)
534
+ if (verbosity === 'text-only') {
535
+ return;
536
+ }
512
537
  if (part.type === 'step-start' || part.type === 'step-finish') {
513
538
  return;
514
539
  }
@@ -840,7 +865,13 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
840
865
  }
841
866
  };
842
867
  const promptResult = await errore.tryAsync(async () => {
843
- const eventHandlerPromise = eventHandler();
868
+ const newHandlerPromise = eventHandler().finally(() => {
869
+ if (activeEventHandlers.get(thread.id) === newHandlerPromise) {
870
+ activeEventHandlers.delete(thread.id);
871
+ }
872
+ });
873
+ activeEventHandlers.set(thread.id, newHandlerPromise);
874
+ handlerPromise = newHandlerPromise;
844
875
  if (abortController.signal.aborted) {
845
876
  sessionLogger.log(`[DEBOUNCE] Aborted before prompt, exiting`);
846
877
  return;
@@ -940,6 +971,14 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
940
971
  }
941
972
  return { sessionID: session.id, result: response.data, port };
942
973
  });
974
+ if (handlerPromise) {
975
+ await Promise.race([
976
+ handlerPromise,
977
+ new Promise((resolve) => {
978
+ setTimeout(resolve, 1000);
979
+ }),
980
+ ]);
981
+ }
943
982
  if (!errore.isError(promptResult)) {
944
983
  return promptResult;
945
984
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.45",
5
+ "version": "0.4.46",
6
6
  "repository": "https://github.com/remorses/kimaki",
7
7
  "bin": "bin.js",
8
8
  "files": [
@@ -88,6 +88,9 @@ export async function handleCreateNewProjectCommand({
88
88
  reason: 'New project session',
89
89
  })
90
90
 
91
+ // Add user to thread so it appears in their sidebar
92
+ await thread.members.add(command.user.id)
93
+
91
94
  await handleOpencodeSession({
92
95
  prompt: 'The project was just initialized. Say hi and ask what the user wants to build.',
93
96
  thread,
@@ -215,6 +215,9 @@ export async function handleForkSelectMenu(
215
215
  reason: `Forked from session ${sessionId}`,
216
216
  })
217
217
 
218
+ // Add user to thread so it appears in their sidebar
219
+ await thread.members.add(interaction.user.id)
220
+
218
221
  getDatabase()
219
222
  .prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
220
223
  .run(thread.id, forkedSession.id)
@@ -73,6 +73,9 @@ export async function handleResumeCommand({ command, appId }: CommandContext): P
73
73
  reason: `Resuming session ${sessionId}`,
74
74
  })
75
75
 
76
+ // Add user to thread so it appears in their sidebar
77
+ await thread.members.add(command.user.id)
78
+
76
79
  getDatabase()
77
80
  .prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
78
81
  .run(thread.id, sessionId)
@@ -75,6 +75,9 @@ export async function handleSessionCommand({ command, appId }: CommandContext):
75
75
  reason: 'OpenCode session',
76
76
  })
77
77
 
78
+ // Add user to thread so it appears in their sidebar
79
+ await thread.members.add(command.user.id)
80
+
78
81
  await command.editReply(`Created new session in ${thread.toString()}`)
79
82
 
80
83
  await handleOpencodeSession({
@@ -136,6 +136,9 @@ export const handleUserCommand: CommandHandler = async ({ command, appId }: Comm
136
136
  reason: `OpenCode command: ${commandName}`,
137
137
  })
138
138
 
139
+ // Add user to thread so it appears in their sidebar
140
+ await newThread.members.add(command.user.id)
141
+
139
142
  await command.editReply(`Started /${commandName} in ${newThread.toString()}`)
140
143
 
141
144
  await handleOpencodeSession({
@@ -228,6 +228,9 @@ export async function handleNewWorktreeCommand({
228
228
  reason: 'Worktree session',
229
229
  })
230
230
 
231
+ // Add user to thread so it appears in their sidebar
232
+ await thread.members.add(command.user.id)
233
+
231
234
  return { thread, starterMessage }
232
235
  },
233
236
  catch: (e) => new WorktreeError('Failed to create thread', { cause: e }),
@@ -425,6 +425,9 @@ export async function startDiscordBot({
425
425
  reason: 'Start Claude session',
426
426
  })
427
427
 
428
+ // Add user to thread so it appears in their sidebar
429
+ await thread.members.add(message.author.id)
430
+
428
431
  discordLogger.log(`Created thread "${thread.name}" (${thread.id})`)
429
432
 
430
433
  // Create worktree if worktrees are enabled (CLI flag OR channel setting)
@@ -86,6 +86,8 @@ export type QueuedMessage = {
86
86
  // Key is threadId, value is array of queued messages
87
87
  export const messageQueue = new Map<string, QueuedMessage[]>()
88
88
 
89
+ const activeEventHandlers = new Map<string, Promise<void>>()
90
+
89
91
  export function addToQueue({
90
92
  threadId,
91
93
  message,
@@ -301,6 +303,15 @@ export async function handleOpencodeSession({
301
303
  if (existingController) {
302
304
  voiceLogger.log(`[ABORT] Cancelling existing request for session: ${session.id}`)
303
305
  existingController.abort(new Error('New request started'))
306
+ const abortResult = await errore.tryAsync(() => {
307
+ return getClient().session.abort({
308
+ path: { id: session.id },
309
+ query: { directory: sdkDirectory },
310
+ })
311
+ })
312
+ if (abortResult instanceof Error) {
313
+ sessionLogger.log(`[ABORT] Server abort failed (may be already done):`, abortResult)
314
+ }
304
315
  }
305
316
 
306
317
  // Auto-reject ALL pending permissions for this thread
@@ -363,6 +374,17 @@ export async function handleOpencodeSession({
363
374
  return
364
375
  }
365
376
 
377
+ const previousHandler = activeEventHandlers.get(thread.id)
378
+ if (previousHandler) {
379
+ sessionLogger.log(`[EVENT] Waiting for previous handler to finish`)
380
+ await Promise.race([
381
+ previousHandler,
382
+ new Promise((resolve) => {
383
+ setTimeout(resolve, 1000)
384
+ }),
385
+ ])
386
+ }
387
+
366
388
  // Use v2 client for event subscription (has proper types for question.asked events)
367
389
  const clientV2 = getOpencodeClientV2(directory)
368
390
  if (!clientV2) {
@@ -398,6 +420,7 @@ export async function handleOpencodeSession({
398
420
  let lastDisplayedContextPercentage = 0
399
421
  let modelContextLimit: number | undefined
400
422
  let assistantMessageId: string | undefined
423
+ let handlerPromise: Promise<void> | null = null
401
424
 
402
425
  let typingInterval: NodeJS.Timeout | null = null
403
426
 
@@ -726,6 +749,10 @@ export async function handleOpencodeSession({
726
749
  part: Part,
727
750
  subtaskInfo: { label: string; assistantMessageId?: string },
728
751
  ) => {
752
+ // In text-only mode, skip all subtask output (they're tool-related)
753
+ if (verbosity === 'text-only') {
754
+ return
755
+ }
729
756
  if (part.type === 'step-start' || part.type === 'step-finish') {
730
757
  return
731
758
  }
@@ -1151,7 +1178,13 @@ export async function handleOpencodeSession({
1151
1178
 
1152
1179
  const promptResult: Error | { sessionID: string; result: any; port?: number } | undefined =
1153
1180
  await errore.tryAsync(async () => {
1154
- const eventHandlerPromise = eventHandler()
1181
+ const newHandlerPromise = eventHandler().finally(() => {
1182
+ if (activeEventHandlers.get(thread.id) === newHandlerPromise) {
1183
+ activeEventHandlers.delete(thread.id)
1184
+ }
1185
+ })
1186
+ activeEventHandlers.set(thread.id, newHandlerPromise)
1187
+ handlerPromise = newHandlerPromise
1155
1188
 
1156
1189
  if (abortController.signal.aborted) {
1157
1190
  sessionLogger.log(`[DEBOUNCE] Aborted before prompt, exiting`)
@@ -1273,6 +1306,15 @@ export async function handleOpencodeSession({
1273
1306
  return { sessionID: session.id, result: response.data, port }
1274
1307
  })
1275
1308
 
1309
+ if (handlerPromise) {
1310
+ await Promise.race([
1311
+ handlerPromise,
1312
+ new Promise((resolve) => {
1313
+ setTimeout(resolve, 1000)
1314
+ }),
1315
+ ])
1316
+ }
1317
+
1276
1318
  if (!errore.isError(promptResult)) {
1277
1319
  return promptResult
1278
1320
  }