kimaki 0.4.19 → 0.4.20

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.
@@ -1039,14 +1039,19 @@ function formatPart(part) {
1039
1039
  const isSingleLine = !command.includes('\n');
1040
1040
  const hasBackticks = command.includes('`');
1041
1041
  if (isSingleLine && command.length <= 120 && !hasBackticks) {
1042
- toolTitle = `\`${command}\``;
1042
+ toolTitle = `_${command}_`;
1043
1043
  }
1044
1044
  else {
1045
- toolTitle = stateTitle ? `*${stateTitle}*` : '';
1045
+ toolTitle = stateTitle ? `_${stateTitle}_` : '';
1046
1046
  }
1047
1047
  }
1048
+ else if (part.tool === 'edit' || part.tool === 'write') {
1049
+ const filePath = part.state.input?.filePath || '';
1050
+ const fileName = filePath.split('/').pop() || filePath;
1051
+ toolTitle = fileName ? `_${fileName}_` : '';
1052
+ }
1048
1053
  else if (stateTitle) {
1049
- toolTitle = `*${stateTitle}*`;
1054
+ toolTitle = `_${stateTitle}_`;
1050
1055
  }
1051
1056
  const icon = part.state.status === 'error' ? '⨯' : '◼︎';
1052
1057
  return `${icon} ${part.tool} ${toolTitle} ${summaryText}`;
@@ -1074,16 +1079,6 @@ async function handleOpencodeSession({ prompt, thread, projectDirectory, origina
1074
1079
  voiceLogger.log(`[OPENCODE SESSION] Starting for thread ${thread.id} with prompt: "${prompt.slice(0, 50)}${prompt.length > 50 ? '...' : ''}"`);
1075
1080
  // Track session start time
1076
1081
  const sessionStartTime = Date.now();
1077
- // Add processing reaction to original message
1078
- if (originalMessage) {
1079
- try {
1080
- await originalMessage.react('⏳');
1081
- discordLogger.log(`Added processing reaction to message`);
1082
- }
1083
- catch (e) {
1084
- discordLogger.log(`Could not add processing reaction:`, e);
1085
- }
1086
- }
1087
1082
  // Use default directory if not specified
1088
1083
  const directory = projectDirectory || process.cwd();
1089
1084
  sessionLogger.log(`Using directory: ${directory}`);
@@ -1134,34 +1129,32 @@ async function handleOpencodeSession({ prompt, thread, projectDirectory, origina
1134
1129
  voiceLogger.log(`[ABORT] Cancelling existing request for session: ${session.id}`);
1135
1130
  existingController.abort(new Error('New request started'));
1136
1131
  }
1137
- if (abortControllers.has(session.id)) {
1138
- abortControllers.get(session.id)?.abort(new Error('new reply'));
1139
- }
1140
1132
  const abortController = new AbortController();
1141
- // Store this controller for this session
1142
1133
  abortControllers.set(session.id, abortController);
1134
+ if (existingController) {
1135
+ await new Promise((resolve) => { setTimeout(resolve, 200); });
1136
+ if (abortController.signal.aborted) {
1137
+ sessionLogger.log(`[DEBOUNCE] Request was superseded during wait, exiting`);
1138
+ return;
1139
+ }
1140
+ }
1141
+ if (abortController.signal.aborted) {
1142
+ sessionLogger.log(`[DEBOUNCE] Aborted before subscribe, exiting`);
1143
+ return;
1144
+ }
1143
1145
  const eventsResult = await getClient().event.subscribe({
1144
1146
  signal: abortController.signal,
1145
1147
  });
1148
+ if (abortController.signal.aborted) {
1149
+ sessionLogger.log(`[DEBOUNCE] Aborted during subscribe, exiting`);
1150
+ return;
1151
+ }
1146
1152
  const events = eventsResult.stream;
1147
1153
  sessionLogger.log(`Subscribed to OpenCode events`);
1148
- // Load existing part-message mappings from database
1149
- const partIdToMessage = new Map();
1150
- const existingParts = getDatabase()
1151
- .prepare('SELECT part_id, message_id FROM part_messages WHERE thread_id = ?')
1152
- .all(thread.id);
1153
- // Pre-populate map with existing messages
1154
- for (const row of existingParts) {
1155
- try {
1156
- const message = await thread.messages.fetch(row.message_id);
1157
- if (message) {
1158
- partIdToMessage.set(row.part_id, message);
1159
- }
1160
- }
1161
- catch (error) {
1162
- voiceLogger.log(`Could not fetch message ${row.message_id} for part ${row.part_id}`);
1163
- }
1164
- }
1154
+ const sentPartIds = new Set(getDatabase()
1155
+ .prepare('SELECT part_id FROM part_messages WHERE thread_id = ?')
1156
+ .all(thread.id)
1157
+ .map((row) => row.part_id));
1165
1158
  let currentParts = [];
1166
1159
  let stopTyping = null;
1167
1160
  let usedModel;
@@ -1174,12 +1167,12 @@ async function handleOpencodeSession({ prompt, thread, projectDirectory, origina
1174
1167
  return;
1175
1168
  }
1176
1169
  // Skip if already sent
1177
- if (partIdToMessage.has(part.id)) {
1170
+ if (sentPartIds.has(part.id)) {
1178
1171
  return;
1179
1172
  }
1180
1173
  try {
1181
1174
  const firstMessage = await sendThreadMessage(thread, content);
1182
- partIdToMessage.set(part.id, firstMessage);
1175
+ sentPartIds.add(part.id);
1183
1176
  // Store part-message mapping in database
1184
1177
  getDatabase()
1185
1178
  .prepare('INSERT OR REPLACE INTO part_messages (part_id, message_id, thread_id) VALUES (?, ?, ?)')
@@ -1275,6 +1268,10 @@ async function handleOpencodeSession({ prompt, thread, projectDirectory, origina
1275
1268
  if (part.type === 'tool' && part.state.status === 'running') {
1276
1269
  await sendPartMessage(part);
1277
1270
  }
1271
+ // Send reasoning parts immediately (shows "◼︎ thinking" indicator early)
1272
+ if (part.type === 'reasoning') {
1273
+ await sendPartMessage(part);
1274
+ }
1278
1275
  // Check if this is a step-finish part
1279
1276
  if (part.type === 'step-finish') {
1280
1277
  // Send all parts accumulated so far to Discord
@@ -1361,7 +1358,7 @@ async function handleOpencodeSession({ prompt, thread, projectDirectory, origina
1361
1358
  finally {
1362
1359
  // Send any remaining parts that weren't sent
1363
1360
  for (const part of currentParts) {
1364
- if (!partIdToMessage.has(part.id)) {
1361
+ if (!sentPartIds.has(part.id)) {
1365
1362
  try {
1366
1363
  await sendPartMessage(part);
1367
1364
  }
@@ -1403,8 +1400,19 @@ async function handleOpencodeSession({ prompt, thread, projectDirectory, origina
1403
1400
  }
1404
1401
  };
1405
1402
  try {
1406
- // Start the event handler
1407
1403
  const eventHandlerPromise = eventHandler();
1404
+ if (abortController.signal.aborted) {
1405
+ sessionLogger.log(`[DEBOUNCE] Aborted before prompt, exiting`);
1406
+ return;
1407
+ }
1408
+ if (originalMessage) {
1409
+ try {
1410
+ await originalMessage.react('⏳');
1411
+ }
1412
+ catch (e) {
1413
+ discordLogger.log(`Could not add processing reaction:`, e);
1414
+ }
1415
+ }
1408
1416
  let response;
1409
1417
  if (parsedCommand?.isCommand) {
1410
1418
  sessionLogger.log(`[COMMAND] Sending command /${parsedCommand.command} to session ${session.id} with args: "${parsedCommand.arguments.slice(0, 100)}${parsedCommand.arguments.length > 100 ? '...' : ''}"`);
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.19",
5
+ "version": "0.4.20",
6
6
  "scripts": {
7
7
  "dev": "tsx --env-file .env src/cli.ts",
8
8
  "prepublishOnly": "pnpm tsc",
@@ -21,7 +21,7 @@
21
21
  "bin.js"
22
22
  ],
23
23
  "devDependencies": {
24
- "@opencode-ai/plugin": "^1.0.169",
24
+ "@opencode-ai/plugin": "^1.0.193",
25
25
  "@types/better-sqlite3": "^7.6.13",
26
26
  "@types/bun": "latest",
27
27
  "@types/js-yaml": "^4.0.9",
@@ -35,7 +35,7 @@
35
35
  "@discordjs/opus": "^0.10.0",
36
36
  "@discordjs/voice": "^0.19.0",
37
37
  "@google/genai": "^1.34.0",
38
- "@opencode-ai/sdk": "^1.0.169",
38
+ "@opencode-ai/sdk": "^1.0.193",
39
39
  "@purinton/resampler": "^1.0.4",
40
40
  "@snazzah/davey": "^0.1.6",
41
41
  "ai": "^5.0.114",
package/src/discordBot.ts CHANGED
@@ -1375,12 +1375,16 @@ function formatPart(part: Part): string {
1375
1375
  const isSingleLine = !command.includes('\n')
1376
1376
  const hasBackticks = command.includes('`')
1377
1377
  if (isSingleLine && command.length <= 120 && !hasBackticks) {
1378
- toolTitle = `\`${command}\``
1378
+ toolTitle = `_${command}_`
1379
1379
  } else {
1380
- toolTitle = stateTitle ? `*${stateTitle}*` : ''
1380
+ toolTitle = stateTitle ? `_${stateTitle}_` : ''
1381
1381
  }
1382
+ } else if (part.tool === 'edit' || part.tool === 'write') {
1383
+ const filePath = (part.state.input?.filePath as string) || ''
1384
+ const fileName = filePath.split('/').pop() || filePath
1385
+ toolTitle = fileName ? `_${fileName}_` : ''
1382
1386
  } else if (stateTitle) {
1383
- toolTitle = `*${stateTitle}*`
1387
+ toolTitle = `_${stateTitle}_`
1384
1388
  }
1385
1389
 
1386
1390
  const icon = part.state.status === 'error' ? '⨯' : '◼︎'
@@ -1430,16 +1434,6 @@ async function handleOpencodeSession({
1430
1434
  // Track session start time
1431
1435
  const sessionStartTime = Date.now()
1432
1436
 
1433
- // Add processing reaction to original message
1434
- if (originalMessage) {
1435
- try {
1436
- await originalMessage.react('⏳')
1437
- discordLogger.log(`Added processing reaction to message`)
1438
- } catch (e) {
1439
- discordLogger.log(`Could not add processing reaction:`, e)
1440
- }
1441
- }
1442
-
1443
1437
  // Use default directory if not specified
1444
1438
  const directory = projectDirectory || process.cwd()
1445
1439
  sessionLogger.log(`Using directory: ${directory}`)
@@ -1507,40 +1501,40 @@ async function handleOpencodeSession({
1507
1501
  existingController.abort(new Error('New request started'))
1508
1502
  }
1509
1503
 
1510
- if (abortControllers.has(session.id)) {
1511
- abortControllers.get(session.id)?.abort(new Error('new reply'))
1512
- }
1513
1504
  const abortController = new AbortController()
1514
- // Store this controller for this session
1515
1505
  abortControllers.set(session.id, abortController)
1516
1506
 
1507
+ if (existingController) {
1508
+ await new Promise((resolve) => { setTimeout(resolve, 200) })
1509
+ if (abortController.signal.aborted) {
1510
+ sessionLogger.log(`[DEBOUNCE] Request was superseded during wait, exiting`)
1511
+ return
1512
+ }
1513
+ }
1514
+
1515
+ if (abortController.signal.aborted) {
1516
+ sessionLogger.log(`[DEBOUNCE] Aborted before subscribe, exiting`)
1517
+ return
1518
+ }
1519
+
1517
1520
  const eventsResult = await getClient().event.subscribe({
1518
1521
  signal: abortController.signal,
1519
1522
  })
1523
+
1524
+ if (abortController.signal.aborted) {
1525
+ sessionLogger.log(`[DEBOUNCE] Aborted during subscribe, exiting`)
1526
+ return
1527
+ }
1528
+
1520
1529
  const events = eventsResult.stream
1521
1530
  sessionLogger.log(`Subscribed to OpenCode events`)
1522
1531
 
1523
- // Load existing part-message mappings from database
1524
- const partIdToMessage = new Map<string, Message>()
1525
- const existingParts = getDatabase()
1526
- .prepare(
1527
- 'SELECT part_id, message_id FROM part_messages WHERE thread_id = ?',
1528
- )
1529
- .all(thread.id) as { part_id: string; message_id: string }[]
1530
-
1531
- // Pre-populate map with existing messages
1532
- for (const row of existingParts) {
1533
- try {
1534
- const message = await thread.messages.fetch(row.message_id)
1535
- if (message) {
1536
- partIdToMessage.set(row.part_id, message)
1537
- }
1538
- } catch (error) {
1539
- voiceLogger.log(
1540
- `Could not fetch message ${row.message_id} for part ${row.part_id}`,
1541
- )
1542
- }
1543
- }
1532
+ const sentPartIds = new Set<string>(
1533
+ (getDatabase()
1534
+ .prepare('SELECT part_id FROM part_messages WHERE thread_id = ?')
1535
+ .all(thread.id) as { part_id: string }[])
1536
+ .map((row) => row.part_id)
1537
+ )
1544
1538
 
1545
1539
  let currentParts: Part[] = []
1546
1540
  let stopTyping: (() => void) | null = null
@@ -1556,13 +1550,13 @@ async function handleOpencodeSession({
1556
1550
  }
1557
1551
 
1558
1552
  // Skip if already sent
1559
- if (partIdToMessage.has(part.id)) {
1553
+ if (sentPartIds.has(part.id)) {
1560
1554
  return
1561
1555
  }
1562
1556
 
1563
1557
  try {
1564
1558
  const firstMessage = await sendThreadMessage(thread, content)
1565
- partIdToMessage.set(part.id, firstMessage)
1559
+ sentPartIds.add(part.id)
1566
1560
 
1567
1561
  // Store part-message mapping in database
1568
1562
  getDatabase()
@@ -1686,6 +1680,11 @@ async function handleOpencodeSession({
1686
1680
  await sendPartMessage(part)
1687
1681
  }
1688
1682
 
1683
+ // Send reasoning parts immediately (shows "◼︎ thinking" indicator early)
1684
+ if (part.type === 'reasoning') {
1685
+ await sendPartMessage(part)
1686
+ }
1687
+
1689
1688
  // Check if this is a step-finish part
1690
1689
  if (part.type === 'step-finish') {
1691
1690
 
@@ -1790,7 +1789,7 @@ async function handleOpencodeSession({
1790
1789
  } finally {
1791
1790
  // Send any remaining parts that weren't sent
1792
1791
  for (const part of currentParts) {
1793
- if (!partIdToMessage.has(part.id)) {
1792
+ if (!sentPartIds.has(part.id)) {
1794
1793
  try {
1795
1794
  await sendPartMessage(part)
1796
1795
  } catch (error) {
@@ -1841,9 +1840,21 @@ async function handleOpencodeSession({
1841
1840
  }
1842
1841
 
1843
1842
  try {
1844
- // Start the event handler
1845
1843
  const eventHandlerPromise = eventHandler()
1846
1844
 
1845
+ if (abortController.signal.aborted) {
1846
+ sessionLogger.log(`[DEBOUNCE] Aborted before prompt, exiting`)
1847
+ return
1848
+ }
1849
+
1850
+ if (originalMessage) {
1851
+ try {
1852
+ await originalMessage.react('⏳')
1853
+ } catch (e) {
1854
+ discordLogger.log(`Could not add processing reaction:`, e)
1855
+ }
1856
+ }
1857
+
1847
1858
  let response: { data?: unknown; error?: unknown; response: Response }
1848
1859
  if (parsedCommand?.isCommand) {
1849
1860
  sessionLogger.log(