kimaki 0.4.46 → 0.4.47
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/cli.js +27 -2
- package/dist/commands/abort.js +2 -2
- package/dist/commands/add-project.js +2 -2
- package/dist/commands/agent.js +4 -4
- package/dist/commands/ask-question.js +9 -8
- package/dist/commands/compact.js +126 -0
- package/dist/commands/create-new-project.js +3 -3
- package/dist/commands/fork.js +3 -3
- package/dist/commands/merge-worktree.js +2 -2
- package/dist/commands/model.js +5 -5
- package/dist/commands/permissions.js +2 -2
- package/dist/commands/queue.js +2 -2
- package/dist/commands/remove-project.js +2 -2
- package/dist/commands/resume.js +2 -2
- package/dist/commands/session.js +2 -2
- package/dist/commands/share.js +2 -2
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/user-command.js +2 -2
- package/dist/commands/verbosity.js +3 -3
- package/dist/commands/worktree-settings.js +2 -2
- package/dist/commands/worktree.js +18 -8
- package/dist/database.js +2 -2
- package/dist/discord-bot.js +3 -3
- package/dist/discord-utils.js +2 -2
- package/dist/genai-worker-wrapper.js +3 -3
- package/dist/genai-worker.js +2 -2
- package/dist/genai.js +2 -2
- package/dist/interaction-handler.js +6 -2
- package/dist/logger.js +57 -9
- package/dist/markdown.js +2 -2
- package/dist/message-formatting.js +69 -6
- package/dist/openai-realtime.js +2 -2
- package/dist/opencode.js +2 -2
- package/dist/session-handler.js +53 -14
- package/dist/tools.js +2 -2
- package/dist/voice-handler.js +2 -2
- package/dist/voice.js +2 -2
- package/dist/worktree-utils.js +91 -7
- package/dist/xml.js +2 -2
- package/package.json +1 -1
- package/src/cli.ts +28 -2
- package/src/commands/abort.ts +2 -2
- package/src/commands/add-project.ts +2 -2
- package/src/commands/agent.ts +4 -4
- package/src/commands/ask-question.ts +9 -8
- package/src/commands/compact.ts +148 -0
- package/src/commands/create-new-project.ts +3 -3
- package/src/commands/fork.ts +3 -3
- package/src/commands/merge-worktree.ts +2 -2
- package/src/commands/model.ts +5 -5
- package/src/commands/permissions.ts +2 -2
- package/src/commands/queue.ts +2 -2
- package/src/commands/remove-project.ts +2 -2
- package/src/commands/resume.ts +2 -2
- package/src/commands/session.ts +2 -2
- package/src/commands/share.ts +2 -2
- package/src/commands/undo-redo.ts +2 -2
- package/src/commands/user-command.ts +2 -2
- package/src/commands/verbosity.ts +3 -3
- package/src/commands/worktree-settings.ts +2 -2
- package/src/commands/worktree.ts +20 -7
- package/src/database.ts +2 -2
- package/src/discord-bot.ts +3 -3
- package/src/discord-utils.ts +2 -2
- package/src/genai-worker-wrapper.ts +3 -3
- package/src/genai-worker.ts +2 -2
- package/src/genai.ts +2 -2
- package/src/interaction-handler.ts +7 -2
- package/src/logger.ts +64 -10
- package/src/markdown.ts +2 -2
- package/src/message-formatting.ts +82 -6
- package/src/openai-realtime.ts +2 -2
- package/src/opencode.ts +2 -2
- package/src/session-handler.ts +62 -14
- package/src/tools.ts +2 -2
- package/src/voice-handler.ts +2 -2
- package/src/voice.ts +2 -2
- package/src/worktree-utils.ts +111 -7
- package/src/xml.ts +2 -2
package/src/database.ts
CHANGED
|
@@ -6,10 +6,10 @@ import Database from 'better-sqlite3'
|
|
|
6
6
|
import fs from 'node:fs'
|
|
7
7
|
import path from 'node:path'
|
|
8
8
|
import * as errore from 'errore'
|
|
9
|
-
import { createLogger } from './logger.js'
|
|
9
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
10
10
|
import { getDataDir } from './config.js'
|
|
11
11
|
|
|
12
|
-
const dbLogger = createLogger(
|
|
12
|
+
const dbLogger = createLogger(LogPrefix.DB)
|
|
13
13
|
|
|
14
14
|
let db: Database.Database | null = null
|
|
15
15
|
|
package/src/discord-bot.ts
CHANGED
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
} from 'discord.js'
|
|
67
67
|
import fs from 'node:fs'
|
|
68
68
|
import * as errore from 'errore'
|
|
69
|
-
import { createLogger } from './logger.js'
|
|
69
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
70
70
|
import { setGlobalDispatcher, Agent } from 'undici'
|
|
71
71
|
|
|
72
72
|
// Increase connection pool to prevent deadlock when multiple sessions have open SSE streams.
|
|
@@ -74,8 +74,8 @@ import { setGlobalDispatcher, Agent } from 'undici'
|
|
|
74
74
|
// regular HTTP requests (question.reply, session.prompt) get blocked → deadlock.
|
|
75
75
|
setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0, connections: 500 }))
|
|
76
76
|
|
|
77
|
-
const discordLogger = createLogger(
|
|
78
|
-
const voiceLogger = createLogger(
|
|
77
|
+
const discordLogger = createLogger(LogPrefix.DISCORD)
|
|
78
|
+
const voiceLogger = createLogger(LogPrefix.VOICE)
|
|
79
79
|
|
|
80
80
|
type StartOptions = {
|
|
81
81
|
token: string
|
package/src/discord-utils.ts
CHANGED
|
@@ -8,12 +8,12 @@ import { formatMarkdownTables } from './format-tables.js'
|
|
|
8
8
|
import { getChannelDirectory } from './database.js'
|
|
9
9
|
import { limitHeadingDepth } from './limit-heading-depth.js'
|
|
10
10
|
import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js'
|
|
11
|
-
import { createLogger } from './logger.js'
|
|
11
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
12
12
|
import mime from 'mime'
|
|
13
13
|
import fs from 'node:fs'
|
|
14
14
|
import path from 'node:path'
|
|
15
15
|
|
|
16
|
-
const discordLogger = createLogger(
|
|
16
|
+
const discordLogger = createLogger(LogPrefix.DISCORD)
|
|
17
17
|
|
|
18
18
|
export const SILENT_MESSAGE_FLAGS = 4 | 4096
|
|
19
19
|
// Same as SILENT but without SuppressNotifications - triggers badge/notification
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
import { Worker } from 'node:worker_threads'
|
|
6
6
|
import type { WorkerInMessage, WorkerOutMessage } from './worker-types.js'
|
|
7
7
|
import type { Tool as AITool } from 'ai'
|
|
8
|
-
import { createLogger } from './logger.js'
|
|
8
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
9
9
|
|
|
10
|
-
const genaiWorkerLogger = createLogger(
|
|
11
|
-
const genaiWrapperLogger = createLogger(
|
|
10
|
+
const genaiWorkerLogger = createLogger(LogPrefix.GENAI_WORKER)
|
|
11
|
+
const genaiWrapperLogger = createLogger(LogPrefix.GENAI_WORKER)
|
|
12
12
|
|
|
13
13
|
export interface GenAIWorkerOptions {
|
|
14
14
|
directory: string
|
package/src/genai-worker.ts
CHANGED
|
@@ -13,13 +13,13 @@ import type { Session } from '@google/genai'
|
|
|
13
13
|
import { getTools } from './tools.js'
|
|
14
14
|
import { mkdir } from 'node:fs/promises'
|
|
15
15
|
import type { WorkerInMessage, WorkerOutMessage } from './worker-types.js'
|
|
16
|
-
import { createLogger } from './logger.js'
|
|
16
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
17
17
|
|
|
18
18
|
if (!parentPort) {
|
|
19
19
|
throw new Error('This module must be run as a worker thread')
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const workerLogger = createLogger(
|
|
22
|
+
const workerLogger = createLogger(`${LogPrefix.WORKER}_${threadId}`)
|
|
23
23
|
workerLogger.log('GenAI worker started')
|
|
24
24
|
|
|
25
25
|
// Define sendError early so it can be used by global handlers
|
package/src/genai.ts
CHANGED
|
@@ -7,10 +7,10 @@ import type { CallableTool } from '@google/genai'
|
|
|
7
7
|
import { writeFile } from 'fs'
|
|
8
8
|
import type { Tool as AITool } from 'ai'
|
|
9
9
|
|
|
10
|
-
import { createLogger } from './logger.js'
|
|
10
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
11
11
|
import { aiToolToCallableTool } from './ai-tool-to-genai.js'
|
|
12
12
|
|
|
13
|
-
const genaiLogger = createLogger(
|
|
13
|
+
const genaiLogger = createLogger(LogPrefix.GENAI)
|
|
14
14
|
|
|
15
15
|
const audioParts: Buffer[] = []
|
|
16
16
|
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { handleCreateNewProjectCommand } from './commands/create-new-project.js'
|
|
20
20
|
import { handlePermissionSelectMenu } from './commands/permissions.js'
|
|
21
21
|
import { handleAbortCommand } from './commands/abort.js'
|
|
22
|
+
import { handleCompactCommand } from './commands/compact.js'
|
|
22
23
|
import { handleShareCommand } from './commands/share.js'
|
|
23
24
|
import { handleForkCommand, handleForkSelectMenu } from './commands/fork.js'
|
|
24
25
|
import {
|
|
@@ -32,9 +33,9 @@ import { handleQueueCommand, handleClearQueueCommand } from './commands/queue.js
|
|
|
32
33
|
import { handleUndoCommand, handleRedoCommand } from './commands/undo-redo.js'
|
|
33
34
|
import { handleUserCommand } from './commands/user-command.js'
|
|
34
35
|
import { handleVerbosityCommand } from './commands/verbosity.js'
|
|
35
|
-
import { createLogger } from './logger.js'
|
|
36
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
36
37
|
|
|
37
|
-
const interactionLogger = createLogger(
|
|
38
|
+
const interactionLogger = createLogger(LogPrefix.INTERACTION)
|
|
38
39
|
|
|
39
40
|
export function registerInteractionHandler({
|
|
40
41
|
discordClient,
|
|
@@ -126,6 +127,10 @@ export function registerInteractionHandler({
|
|
|
126
127
|
await handleAbortCommand({ command: interaction, appId })
|
|
127
128
|
return
|
|
128
129
|
|
|
130
|
+
case 'compact':
|
|
131
|
+
await handleCompactCommand({ command: interaction, appId })
|
|
132
|
+
return
|
|
133
|
+
|
|
129
134
|
case 'share':
|
|
130
135
|
await handleShareCommand({ command: interaction, appId })
|
|
131
136
|
return
|
package/src/logger.ts
CHANGED
|
@@ -1,12 +1,54 @@
|
|
|
1
|
-
// Prefixed logging utility
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Prefixed logging utility.
|
|
2
|
+
// Uses picocolors for compact frequent logs (log, info, debug).
|
|
3
|
+
// Uses @clack/prompts only for important events (warn, error) with visual distinction.
|
|
4
4
|
|
|
5
|
-
import { log } from '@clack/prompts'
|
|
5
|
+
import { log as clackLog } from '@clack/prompts'
|
|
6
6
|
import fs from 'node:fs'
|
|
7
7
|
import path, { dirname } from 'node:path'
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
9
|
import util from 'node:util'
|
|
10
|
+
import pc from 'picocolors'
|
|
11
|
+
|
|
12
|
+
// All known log prefixes - add new ones here to keep alignment consistent
|
|
13
|
+
export const LogPrefix = {
|
|
14
|
+
ABORT: 'ABORT',
|
|
15
|
+
ADD_PROJECT: 'ADD_PROJ',
|
|
16
|
+
AGENT: 'AGENT',
|
|
17
|
+
ASK_QUESTION: 'QUESTION',
|
|
18
|
+
CLI: 'CLI',
|
|
19
|
+
COMPACT: 'COMPACT',
|
|
20
|
+
CREATE_PROJECT: 'NEW_PROJ',
|
|
21
|
+
DB: 'DB',
|
|
22
|
+
DISCORD: 'DISCORD',
|
|
23
|
+
FORK: 'FORK',
|
|
24
|
+
FORMATTING: 'FORMAT',
|
|
25
|
+
GENAI: 'GENAI',
|
|
26
|
+
GENAI_WORKER: 'GENAI_W',
|
|
27
|
+
INTERACTION: 'INTERACT',
|
|
28
|
+
MARKDOWN: 'MARKDOWN',
|
|
29
|
+
MODEL: 'MODEL',
|
|
30
|
+
OPENAI: 'OPENAI',
|
|
31
|
+
OPENCODE: 'OPENCODE',
|
|
32
|
+
PERMISSIONS: 'PERMS',
|
|
33
|
+
QUEUE: 'QUEUE',
|
|
34
|
+
REMOVE_PROJECT: 'RM_PROJ',
|
|
35
|
+
RESUME: 'RESUME',
|
|
36
|
+
SESSION: 'SESSION',
|
|
37
|
+
SHARE: 'SHARE',
|
|
38
|
+
TOOLS: 'TOOLS',
|
|
39
|
+
UNDO_REDO: 'UNDO',
|
|
40
|
+
USER_CMD: 'USER_CMD',
|
|
41
|
+
VERBOSITY: 'VERBOSE',
|
|
42
|
+
VOICE: 'VOICE',
|
|
43
|
+
WORKER: 'WORKER',
|
|
44
|
+
WORKTREE: 'WORKTREE',
|
|
45
|
+
XML: 'XML',
|
|
46
|
+
} as const
|
|
47
|
+
|
|
48
|
+
export type LogPrefixType = (typeof LogPrefix)[keyof typeof LogPrefix]
|
|
49
|
+
|
|
50
|
+
// compute max length from all known prefixes for alignment
|
|
51
|
+
const MAX_PREFIX_LENGTH = Math.max(...Object.values(LogPrefix).map((p) => p.length))
|
|
10
52
|
|
|
11
53
|
const __filename = fileURLToPath(import.meta.url)
|
|
12
54
|
const __dirname = dirname(__filename)
|
|
@@ -39,27 +81,39 @@ function writeToFile(level: string, prefix: string, args: unknown[]) {
|
|
|
39
81
|
fs.appendFileSync(logFilePath, message)
|
|
40
82
|
}
|
|
41
83
|
|
|
42
|
-
|
|
84
|
+
function getTimestamp(): string {
|
|
85
|
+
const now = new Date()
|
|
86
|
+
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function padPrefix(prefix: string): string {
|
|
90
|
+
return prefix.padEnd(MAX_PREFIX_LENGTH)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createLogger(prefix: LogPrefixType | string) {
|
|
94
|
+
const paddedPrefix = padPrefix(prefix)
|
|
43
95
|
return {
|
|
44
96
|
log: (...args: unknown[]) => {
|
|
45
97
|
writeToFile('INFO', prefix, args)
|
|
46
|
-
log.
|
|
98
|
+
console.log(pc.dim(getTimestamp()), pc.cyan(paddedPrefix), ...args.map(formatArg))
|
|
47
99
|
},
|
|
48
100
|
error: (...args: unknown[]) => {
|
|
49
101
|
writeToFile('ERROR', prefix, args)
|
|
50
|
-
|
|
102
|
+
// use clack for errors - visually distinct
|
|
103
|
+
clackLog.error([paddedPrefix, ...args.map(formatArg)].join(' '))
|
|
51
104
|
},
|
|
52
105
|
warn: (...args: unknown[]) => {
|
|
53
106
|
writeToFile('WARN', prefix, args)
|
|
54
|
-
|
|
107
|
+
// use clack for warnings - visually distinct
|
|
108
|
+
clackLog.warn([paddedPrefix, ...args.map(formatArg)].join(' '))
|
|
55
109
|
},
|
|
56
110
|
info: (...args: unknown[]) => {
|
|
57
111
|
writeToFile('INFO', prefix, args)
|
|
58
|
-
log.
|
|
112
|
+
console.log(pc.dim(getTimestamp()), pc.blue(paddedPrefix), ...args.map(formatArg))
|
|
59
113
|
},
|
|
60
114
|
debug: (...args: unknown[]) => {
|
|
61
115
|
writeToFile('DEBUG', prefix, args)
|
|
62
|
-
log.
|
|
116
|
+
console.log(pc.dim(getTimestamp()), pc.dim(paddedPrefix), ...args.map(formatArg))
|
|
63
117
|
},
|
|
64
118
|
}
|
|
65
119
|
}
|
package/src/markdown.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { createTaggedError } from 'errore'
|
|
|
9
9
|
import * as yaml from 'js-yaml'
|
|
10
10
|
import { formatDateTime } from './utils.js'
|
|
11
11
|
import { extractNonXmlContent } from './xml.js'
|
|
12
|
-
import { createLogger } from './logger.js'
|
|
12
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
13
13
|
import { SessionNotFoundError, MessagesNotFoundError } from './errors.js'
|
|
14
14
|
|
|
15
15
|
// Generic error for unexpected exceptions in async operations
|
|
@@ -18,7 +18,7 @@ class UnexpectedError extends createTaggedError({
|
|
|
18
18
|
message: '$message',
|
|
19
19
|
}) {}
|
|
20
20
|
|
|
21
|
-
const markdownLogger = createLogger(
|
|
21
|
+
const markdownLogger = createLogger(LogPrefix.MARKDOWN)
|
|
22
22
|
|
|
23
23
|
export class ShareMarkdown {
|
|
24
24
|
constructor(private client: OpencodeClient) {}
|
|
@@ -8,7 +8,7 @@ import type { Message } from 'discord.js'
|
|
|
8
8
|
import fs from 'node:fs'
|
|
9
9
|
import path from 'node:path'
|
|
10
10
|
import * as errore from 'errore'
|
|
11
|
-
import { createLogger } from './logger.js'
|
|
11
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
12
12
|
import { FetchError } from './errors.js'
|
|
13
13
|
|
|
14
14
|
// Generic message type compatible with both v1 and v2 SDK
|
|
@@ -19,7 +19,7 @@ type GenericSessionMessage = {
|
|
|
19
19
|
|
|
20
20
|
const ATTACHMENTS_DIR = path.join(process.cwd(), 'tmp', 'discord-attachments')
|
|
21
21
|
|
|
22
|
-
const logger = createLogger(
|
|
22
|
+
const logger = createLogger(LogPrefix.FORMATTING)
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Escapes Discord inline markdown characters so dynamic content
|
|
@@ -29,6 +29,13 @@ function escapeInlineMarkdown(text: string): string {
|
|
|
29
29
|
return text.replace(/([*_~|`\\])/g, '\\$1')
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Normalize whitespace: convert newlines to spaces and collapse consecutive spaces.
|
|
34
|
+
*/
|
|
35
|
+
function normalizeWhitespace(text: string): string {
|
|
36
|
+
return text.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ')
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
/**
|
|
33
40
|
* Collects and formats the last N assistant parts from session messages.
|
|
34
41
|
* Used by both /resume and /fork to show recent assistant context.
|
|
@@ -170,6 +177,73 @@ export function getToolSummaryText(part: Part): string {
|
|
|
170
177
|
: `(+${added}-${removed})`
|
|
171
178
|
}
|
|
172
179
|
|
|
180
|
+
if (part.tool === 'apply_patch') {
|
|
181
|
+
const state = part.state as {
|
|
182
|
+
metadata?: { files?: unknown; diff?: unknown }
|
|
183
|
+
output?: unknown
|
|
184
|
+
}
|
|
185
|
+
const rawFiles = state.metadata?.files
|
|
186
|
+
const partMetaFiles = (part as { metadata?: { files?: unknown } }).metadata?.files
|
|
187
|
+
const filesList = Array.isArray(rawFiles)
|
|
188
|
+
? rawFiles
|
|
189
|
+
: Array.isArray(partMetaFiles)
|
|
190
|
+
? partMetaFiles
|
|
191
|
+
: []
|
|
192
|
+
|
|
193
|
+
const summarizeFiles = (files: unknown[]): string => {
|
|
194
|
+
const summarized = files
|
|
195
|
+
.map((f) => {
|
|
196
|
+
if (!f) {
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
if (typeof f === 'string') {
|
|
200
|
+
const fileName = f.split('/').pop() || ''
|
|
201
|
+
return fileName ? `*${escapeInlineMarkdown(fileName)}* (+0-0)` : `(+0-0)`
|
|
202
|
+
}
|
|
203
|
+
if (typeof f !== 'object') {
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
const file = f as Record<string, unknown>
|
|
207
|
+
const pathStr = String(file.relativePath || file.filePath || file.path || '')
|
|
208
|
+
const fileName = pathStr.split('/').pop() || ''
|
|
209
|
+
const added = typeof file.additions === 'number' ? file.additions : 0
|
|
210
|
+
const removed = typeof file.deletions === 'number' ? file.deletions : 0
|
|
211
|
+
return fileName
|
|
212
|
+
? `*${escapeInlineMarkdown(fileName)}* (+${added}-${removed})`
|
|
213
|
+
: `(+${added}-${removed})`
|
|
214
|
+
})
|
|
215
|
+
.filter(Boolean)
|
|
216
|
+
.join(', ')
|
|
217
|
+
return summarized
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (filesList.length > 0) {
|
|
221
|
+
const summarized = summarizeFiles(filesList)
|
|
222
|
+
if (summarized) {
|
|
223
|
+
return summarized
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const outputText = typeof state.output === 'string' ? state.output : ''
|
|
228
|
+
const outputLines = outputText.split('\n')
|
|
229
|
+
const updatedIndex = outputLines.findIndex((line) =>
|
|
230
|
+
line.startsWith('Success. Updated the following files:'),
|
|
231
|
+
)
|
|
232
|
+
if (updatedIndex !== -1) {
|
|
233
|
+
const fileLines = outputLines.slice(updatedIndex + 1).filter(Boolean)
|
|
234
|
+
if (fileLines.length > 0) {
|
|
235
|
+
const summarized = summarizeFiles(
|
|
236
|
+
fileLines.map((line) => line.replace(/^[AMD]\s+/, '').trim()),
|
|
237
|
+
)
|
|
238
|
+
if (summarized) {
|
|
239
|
+
return summarized
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return ''
|
|
245
|
+
}
|
|
246
|
+
|
|
173
247
|
if (part.tool === 'write') {
|
|
174
248
|
const filePath = (part.state.input?.filePath as string) || ''
|
|
175
249
|
const content = (part.state.input?.content as string) || ''
|
|
@@ -228,7 +302,8 @@ export function getToolSummaryText(part: Part): string {
|
|
|
228
302
|
.map(([key, value]) => {
|
|
229
303
|
if (value === null || value === undefined) return null
|
|
230
304
|
const stringValue = typeof value === 'string' ? value : JSON.stringify(value)
|
|
231
|
-
const
|
|
305
|
+
const normalized = normalizeWhitespace(stringValue)
|
|
306
|
+
const truncatedValue = normalized.length > 50 ? normalized.slice(0, 50) + '…' : normalized
|
|
232
307
|
return `${key}: ${truncatedValue}`
|
|
233
308
|
})
|
|
234
309
|
.filter(Boolean)
|
|
@@ -259,7 +334,7 @@ export function formatTodoList(part: Part): string {
|
|
|
259
334
|
}
|
|
260
335
|
|
|
261
336
|
export function formatPart(part: Part, prefix?: string): string {
|
|
262
|
-
const pfx = prefix ? `${prefix}
|
|
337
|
+
const pfx = prefix ? `${prefix} ⋅ ` : ''
|
|
263
338
|
|
|
264
339
|
if (part.type === 'text') {
|
|
265
340
|
if (!part.text?.trim()) return ''
|
|
@@ -343,12 +418,13 @@ export function formatPart(part: Part, prefix?: string): string {
|
|
|
343
418
|
if (part.state.status === 'error') {
|
|
344
419
|
return '⨯'
|
|
345
420
|
}
|
|
346
|
-
if (part.tool === 'edit' || part.tool === 'write') {
|
|
421
|
+
if (part.tool === 'edit' || part.tool === 'write' || part.tool === 'apply_patch') {
|
|
347
422
|
return '◼︎'
|
|
348
423
|
}
|
|
349
424
|
return '┣'
|
|
350
425
|
})()
|
|
351
|
-
|
|
426
|
+
const toolParts = [part.tool, toolTitle, summaryText].filter(Boolean).join(' ')
|
|
427
|
+
return `${icon} ${pfx}${toolParts}`
|
|
352
428
|
}
|
|
353
429
|
|
|
354
430
|
logger.warn('Unknown part type:', part)
|
package/src/openai-realtime.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
import { RealtimeClient } from '@openai/realtime-api-beta'
|
|
6
6
|
import { writeFile } from 'fs'
|
|
7
7
|
import type { Tool } from 'ai'
|
|
8
|
-
import { createLogger } from './logger.js'
|
|
8
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
9
9
|
|
|
10
|
-
const openaiLogger = createLogger(
|
|
10
|
+
const openaiLogger = createLogger(LogPrefix.OPENAI)
|
|
11
11
|
|
|
12
12
|
// Export the session type for reuse
|
|
13
13
|
export interface OpenAIRealtimeSession {
|
package/src/opencode.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type OpencodeClient as OpencodeClientV2,
|
|
13
13
|
} from '@opencode-ai/sdk/v2'
|
|
14
14
|
import * as errore from 'errore'
|
|
15
|
-
import { createLogger } from './logger.js'
|
|
15
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
16
16
|
import {
|
|
17
17
|
DirectoryNotAccessibleError,
|
|
18
18
|
ServerStartError,
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
type OpenCodeErrors,
|
|
22
22
|
} from './errors.js'
|
|
23
23
|
|
|
24
|
-
const opencodeLogger = createLogger(
|
|
24
|
+
const opencodeLogger = createLogger(LogPrefix.OPENCODE)
|
|
25
25
|
|
|
26
26
|
const opencodeServers = new Map<
|
|
27
27
|
string,
|
package/src/session-handler.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from './discord-utils.js'
|
|
25
25
|
import { formatPart } from './message-formatting.js'
|
|
26
26
|
import { getOpencodeSystemMessage, type WorktreeInfo } from './system-message.js'
|
|
27
|
-
import { createLogger } from './logger.js'
|
|
27
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
28
28
|
import { isAbortError } from './utils.js'
|
|
29
29
|
import {
|
|
30
30
|
showAskUserQuestionDropdowns,
|
|
@@ -38,9 +38,9 @@ import {
|
|
|
38
38
|
} from './commands/permissions.js'
|
|
39
39
|
import * as errore from 'errore'
|
|
40
40
|
|
|
41
|
-
const sessionLogger = createLogger(
|
|
42
|
-
const voiceLogger = createLogger(
|
|
43
|
-
const discordLogger = createLogger(
|
|
41
|
+
const sessionLogger = createLogger(LogPrefix.SESSION)
|
|
42
|
+
const voiceLogger = createLogger(LogPrefix.VOICE)
|
|
43
|
+
const discordLogger = createLogger(LogPrefix.DISCORD)
|
|
44
44
|
|
|
45
45
|
export const abortControllers = new Map<string, AbortController>()
|
|
46
46
|
|
|
@@ -134,7 +134,7 @@ export async function abortAndRetrySession({
|
|
|
134
134
|
sessionLogger.log(`[ABORT+RETRY] Aborting session ${sessionId} for model change`)
|
|
135
135
|
|
|
136
136
|
// Abort with special reason so we don't show "completed" message
|
|
137
|
-
controller.abort('model-change')
|
|
137
|
+
controller.abort(new Error('model-change'))
|
|
138
138
|
|
|
139
139
|
// Also call the API abort endpoint
|
|
140
140
|
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
@@ -350,10 +350,10 @@ export async function handleOpencodeSession({
|
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
//
|
|
354
|
-
const
|
|
355
|
-
if (
|
|
356
|
-
sessionLogger.log(`[QUESTION]
|
|
353
|
+
// Answer any pending question tool with the user's message (silently, no thread message)
|
|
354
|
+
const questionAnswered = await cancelPendingQuestion(thread.id, prompt)
|
|
355
|
+
if (questionAnswered) {
|
|
356
|
+
sessionLogger.log(`[QUESTION] Answered pending question with user message`)
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
const abortController = new AbortController()
|
|
@@ -418,6 +418,7 @@ export async function handleOpencodeSession({
|
|
|
418
418
|
let usedAgent: string | undefined
|
|
419
419
|
let tokensUsedInSession = 0
|
|
420
420
|
let lastDisplayedContextPercentage = 0
|
|
421
|
+
let lastRateLimitDisplayTime = 0
|
|
421
422
|
let modelContextLimit: number | undefined
|
|
422
423
|
let assistantMessageId: string | undefined
|
|
423
424
|
let handlerPromise: Promise<void> | null = null
|
|
@@ -1011,10 +1012,53 @@ export async function handleOpencodeSession({
|
|
|
1011
1012
|
})
|
|
1012
1013
|
}
|
|
1013
1014
|
|
|
1015
|
+
const handleSessionStatus = async (properties: {
|
|
1016
|
+
sessionID: string
|
|
1017
|
+
status: { type: 'idle' } | { type: 'retry'; attempt: number; message: string; next: number } | { type: 'busy' }
|
|
1018
|
+
}) => {
|
|
1019
|
+
if (properties.sessionID !== session.id) {
|
|
1020
|
+
return
|
|
1021
|
+
}
|
|
1022
|
+
if (properties.status.type !== 'retry') {
|
|
1023
|
+
return
|
|
1024
|
+
}
|
|
1025
|
+
// Throttle to once per 10 seconds
|
|
1026
|
+
const now = Date.now()
|
|
1027
|
+
if (now - lastRateLimitDisplayTime < 10_000) {
|
|
1028
|
+
return
|
|
1029
|
+
}
|
|
1030
|
+
lastRateLimitDisplayTime = now
|
|
1031
|
+
|
|
1032
|
+
const { attempt, message, next } = properties.status
|
|
1033
|
+
const remainingMs = Math.max(0, next - now)
|
|
1034
|
+
const remainingSec = Math.ceil(remainingMs / 1000)
|
|
1035
|
+
|
|
1036
|
+
const duration = (() => {
|
|
1037
|
+
if (remainingSec < 60) {
|
|
1038
|
+
return `${remainingSec}s`
|
|
1039
|
+
}
|
|
1040
|
+
const mins = Math.floor(remainingSec / 60)
|
|
1041
|
+
const secs = remainingSec % 60
|
|
1042
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`
|
|
1043
|
+
})()
|
|
1044
|
+
|
|
1045
|
+
const chunk = `⬦ ${message} - retrying in ${duration} (attempt #${attempt})`
|
|
1046
|
+
await thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS })
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1014
1049
|
const handleSessionIdle = (idleSessionId: string) => {
|
|
1015
1050
|
if (idleSessionId === session.id) {
|
|
1051
|
+
// Ignore stale session.idle events - if we haven't received any content yet
|
|
1052
|
+
// (no assistantMessageId set), this is likely a stale event from before
|
|
1053
|
+
// the prompt was sent or from a previous request's subscription state.
|
|
1054
|
+
if (!assistantMessageId) {
|
|
1055
|
+
sessionLogger.log(
|
|
1056
|
+
`[SESSION IDLE] Ignoring stale idle event for ${session.id} (no content received yet)`,
|
|
1057
|
+
)
|
|
1058
|
+
return
|
|
1059
|
+
}
|
|
1016
1060
|
sessionLogger.log(`[SESSION IDLE] Session ${session.id} is idle, aborting`)
|
|
1017
|
-
abortController.abort('finished')
|
|
1061
|
+
abortController.abort(new Error('finished'))
|
|
1018
1062
|
return
|
|
1019
1063
|
}
|
|
1020
1064
|
|
|
@@ -1051,6 +1095,9 @@ export async function handleOpencodeSession({
|
|
|
1051
1095
|
case 'session.idle':
|
|
1052
1096
|
handleSessionIdle(event.properties.sessionID)
|
|
1053
1097
|
break
|
|
1098
|
+
case 'session.status':
|
|
1099
|
+
await handleSessionStatus(event.properties)
|
|
1100
|
+
break
|
|
1054
1101
|
default:
|
|
1055
1102
|
break
|
|
1056
1103
|
}
|
|
@@ -1079,7 +1126,8 @@ export async function handleOpencodeSession({
|
|
|
1079
1126
|
stopTyping = null
|
|
1080
1127
|
}
|
|
1081
1128
|
|
|
1082
|
-
|
|
1129
|
+
const abortReason = (abortController.signal.reason as Error)?.message
|
|
1130
|
+
if (!abortController.signal.aborted || abortReason === 'finished') {
|
|
1083
1131
|
const sessionDuration = prettyMilliseconds(Date.now() - sessionStartTime)
|
|
1084
1132
|
const attachCommand = port ? ` ⋅ ${session.id}` : ''
|
|
1085
1133
|
const modelInfo = usedModel ? ` ⋅ ${usedModel}` : ''
|
|
@@ -1170,7 +1218,7 @@ export async function handleOpencodeSession({
|
|
|
1170
1218
|
}
|
|
1171
1219
|
} else {
|
|
1172
1220
|
sessionLogger.log(
|
|
1173
|
-
`Session was aborted (reason: ${
|
|
1221
|
+
`Session was aborted (reason: ${abortReason}), skipping duration message`,
|
|
1174
1222
|
)
|
|
1175
1223
|
}
|
|
1176
1224
|
}
|
|
@@ -1289,7 +1337,7 @@ export async function handleOpencodeSession({
|
|
|
1289
1337
|
throw new Error(`OpenCode API error (${response.response.status}): ${errorMessage}`)
|
|
1290
1338
|
}
|
|
1291
1339
|
|
|
1292
|
-
abortController.abort('finished')
|
|
1340
|
+
abortController.abort(new Error('finished'))
|
|
1293
1341
|
|
|
1294
1342
|
sessionLogger.log(`Successfully sent prompt, got response`)
|
|
1295
1343
|
|
|
@@ -1325,7 +1373,7 @@ export async function handleOpencodeSession({
|
|
|
1325
1373
|
}
|
|
1326
1374
|
|
|
1327
1375
|
sessionLogger.error(`ERROR: Failed to send prompt:`, promptError)
|
|
1328
|
-
abortController.abort('error')
|
|
1376
|
+
abortController.abort(new Error('error'))
|
|
1329
1377
|
|
|
1330
1378
|
if (originalMessage) {
|
|
1331
1379
|
const reactionResult = await errore.tryAsync(async () => {
|
package/src/tools.ts
CHANGED
|
@@ -12,10 +12,10 @@ import {
|
|
|
12
12
|
type AssistantMessage,
|
|
13
13
|
type Provider,
|
|
14
14
|
} from '@opencode-ai/sdk'
|
|
15
|
-
import { createLogger } from './logger.js'
|
|
15
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
16
16
|
import * as errore from 'errore'
|
|
17
17
|
|
|
18
|
-
const toolsLogger = createLogger(
|
|
18
|
+
const toolsLogger = createLogger(LogPrefix.TOOLS)
|
|
19
19
|
|
|
20
20
|
import { ShareMarkdown } from './markdown.js'
|
|
21
21
|
import { formatDistanceToNow } from './utils.js'
|
package/src/voice-handler.ts
CHANGED
|
@@ -37,9 +37,9 @@ import {
|
|
|
37
37
|
import { transcribeAudio } from './voice.js'
|
|
38
38
|
import { FetchError } from './errors.js'
|
|
39
39
|
|
|
40
|
-
import { createLogger } from './logger.js'
|
|
40
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
41
41
|
|
|
42
|
-
const voiceLogger = createLogger(
|
|
42
|
+
const voiceLogger = createLogger(LogPrefix.VOICE)
|
|
43
43
|
|
|
44
44
|
export type VoiceConnectionData = {
|
|
45
45
|
connection: VoiceConnection
|
package/src/voice.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { GoogleGenAI, Type, type Content, type Part, type Tool } from '@google/genai'
|
|
7
7
|
import * as errore from 'errore'
|
|
8
|
-
import { createLogger } from './logger.js'
|
|
8
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
9
9
|
import { glob } from 'glob'
|
|
10
10
|
import { ripGrep } from 'ripgrep-js'
|
|
11
11
|
import {
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
GlobSearchError,
|
|
20
20
|
} from './errors.js'
|
|
21
21
|
|
|
22
|
-
const voiceLogger = createLogger(
|
|
22
|
+
const voiceLogger = createLogger(LogPrefix.VOICE)
|
|
23
23
|
|
|
24
24
|
export type TranscriptionToolRunner = ({
|
|
25
25
|
name,
|