kimaki 0.4.44 β 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.
- package/dist/channel-management.js +6 -15
- package/dist/cli.js +54 -37
- package/dist/commands/create-new-project.js +2 -0
- package/dist/commands/fork.js +2 -0
- package/dist/commands/permissions.js +21 -5
- package/dist/commands/queue.js +5 -1
- package/dist/commands/resume.js +10 -16
- package/dist/commands/session.js +20 -42
- package/dist/commands/user-command.js +10 -17
- package/dist/commands/verbosity.js +53 -0
- package/dist/commands/worktree-settings.js +2 -2
- package/dist/commands/worktree.js +134 -25
- package/dist/database.js +49 -0
- package/dist/discord-bot.js +26 -38
- package/dist/discord-utils.js +51 -13
- package/dist/discord-utils.test.js +20 -0
- package/dist/escape-backticks.test.js +14 -3
- package/dist/interaction-handler.js +4 -0
- package/dist/session-handler.js +581 -414
- package/package.json +1 -1
- package/src/__snapshots__/first-session-no-info.md +1344 -0
- package/src/__snapshots__/first-session-with-info.md +1350 -0
- package/src/__snapshots__/session-1.md +1344 -0
- package/src/__snapshots__/session-2.md +291 -0
- package/src/__snapshots__/session-3.md +20324 -0
- package/src/__snapshots__/session-with-tools.md +1344 -0
- package/src/channel-management.ts +6 -17
- package/src/cli.ts +63 -45
- package/src/commands/create-new-project.ts +3 -0
- package/src/commands/fork.ts +3 -0
- package/src/commands/permissions.ts +31 -5
- package/src/commands/queue.ts +5 -1
- package/src/commands/resume.ts +11 -18
- package/src/commands/session.ts +21 -44
- package/src/commands/user-command.ts +11 -19
- package/src/commands/verbosity.ts +71 -0
- package/src/commands/worktree-settings.ts +2 -2
- package/src/commands/worktree.ts +163 -27
- package/src/database.ts +65 -0
- package/src/discord-bot.ts +29 -42
- package/src/discord-utils.test.ts +23 -0
- package/src/discord-utils.ts +52 -13
- package/src/escape-backticks.test.ts +14 -3
- package/src/interaction-handler.ts +5 -0
- package/src/session-handler.ts +711 -436
|
@@ -0,0 +1,1350 @@
|
|
|
1
|
+
# see in this codebase where we show dropdown for permissions requests for path...
|
|
2
|
+
|
|
3
|
+
## Session Information
|
|
4
|
+
|
|
5
|
+
- **Created**: Jan 25, 2026, 1:37 PM
|
|
6
|
+
- **Updated**: Jan 25, 2026, 1:37 PM
|
|
7
|
+
- **OpenCode Version**: v1.1.35
|
|
8
|
+
|
|
9
|
+
## Conversation
|
|
10
|
+
|
|
11
|
+
### π€ User
|
|
12
|
+
|
|
13
|
+
see in this codebase where we show dropdown for permissions requests for paths. right now we somtimes show duplicate dropdowns for the same patterns if the opencode sdk return many permissions requests for the same patterns. we should deduplicate them first instead.
|
|
14
|
+
|
|
15
|
+
create a plan. so that same pattern requests do not show 2 dropdowns
|
|
16
|
+
|
|
17
|
+
[Current branch: main]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### π€ Assistant (gpt-5.2-codex)
|
|
21
|
+
|
|
22
|
+
**Started using openai/gpt-5.2-codex**
|
|
23
|
+
|
|
24
|
+
#### π οΈ Tool: bash
|
|
25
|
+
|
|
26
|
+
**Input:**
|
|
27
|
+
```yaml
|
|
28
|
+
command: git ls-files | tree --fromfile
|
|
29
|
+
description: List tracked files in repo
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Output:**
|
|
34
|
+
```
|
|
35
|
+
.
|
|
36
|
+
βββ .gitignore
|
|
37
|
+
βββ .gitmodules
|
|
38
|
+
βββ .oxfmtrc.jsonc
|
|
39
|
+
βββ .prettierrc.json
|
|
40
|
+
βββ AGENTS.md
|
|
41
|
+
βββ CLAUDE.md
|
|
42
|
+
βββ discord
|
|
43
|
+
βΒ Β βββ bin.js
|
|
44
|
+
βΒ Β βββ bin.sh
|
|
45
|
+
βΒ Β βββ CHANGELOG.md
|
|
46
|
+
βΒ Β βββ package.json
|
|
47
|
+
βΒ Β βββ README.md
|
|
48
|
+
βΒ Β βββ scripts
|
|
49
|
+
βΒ Β βΒ Β βββ get-last-session-messages.ts
|
|
50
|
+
βΒ Β βΒ Β βββ list-projects.ts
|
|
51
|
+
βΒ Β βΒ Β βββ pcm-to-mp3.ts
|
|
52
|
+
βΒ Β βΒ Β βββ test-genai.ts
|
|
53
|
+
βΒ Β βΒ Β βββ test-model-id.ts
|
|
54
|
+
βΒ Β βΒ Β βββ test-voice-genai.ts
|
|
55
|
+
βΒ Β βββ src
|
|
56
|
+
βΒ Β βΒ Β βββ __snapshots__
|
|
57
|
+
βΒ Β βΒ Β βΒ Β βββ compact-session-context-no-system.md
|
|
58
|
+
βΒ Β βΒ Β βΒ Β βββ compact-session-context.md
|
|
59
|
+
βΒ Β βΒ Β βββ ai-tool-to-genai.test.ts
|
|
60
|
+
βΒ Β βΒ Β βββ ai-tool-to-genai.ts
|
|
61
|
+
βΒ Β βΒ Β βββ channel-management.ts
|
|
62
|
+
βΒ Β βΒ Β βββ cli.ts
|
|
63
|
+
βΒ Β βΒ Β βββ commands
|
|
64
|
+
βΒ Β βΒ Β βΒ Β βββ abort.ts
|
|
65
|
+
βΒ Β βΒ Β βΒ Β βββ add-project.ts
|
|
66
|
+
βΒ Β βΒ Β βΒ Β βββ agent.ts
|
|
67
|
+
βΒ Β βΒ Β βΒ Β βββ ask-question.ts
|
|
68
|
+
βΒ Β βΒ Β βΒ Β βββ create-new-project.ts
|
|
69
|
+
βΒ Β βΒ Β βΒ Β βββ fork.ts
|
|
70
|
+
βΒ Β βΒ Β βΒ Β βββ merge-worktree.ts
|
|
71
|
+
βΒ Β βΒ Β βΒ Β βββ model.ts
|
|
72
|
+
βΒ Β βΒ Β βΒ Β βββ permissions.ts
|
|
73
|
+
βΒ Β βΒ Β βΒ Β βββ queue.ts
|
|
74
|
+
βΒ Β βΒ Β βΒ Β βββ remove-project.ts
|
|
75
|
+
βΒ Β βΒ Β βΒ Β βββ resume.ts
|
|
76
|
+
βΒ Β βΒ Β βΒ Β βββ session.ts
|
|
77
|
+
βΒ Β βΒ Β βΒ Β βββ share.ts
|
|
78
|
+
βΒ Β βΒ Β βΒ Β βββ types.ts
|
|
79
|
+
βΒ Β βΒ Β βΒ Β βββ undo-redo.ts
|
|
80
|
+
βΒ Β βΒ Β βΒ Β βββ user-command.ts
|
|
81
|
+
βΒ Β βΒ Β βΒ Β βββ worktree-settings.ts
|
|
82
|
+
βΒ Β βΒ Β βΒ Β βββ worktree.ts
|
|
83
|
+
βΒ Β βΒ Β βββ config.ts
|
|
84
|
+
βΒ Β βΒ Β βββ database.ts
|
|
85
|
+
βΒ Β βΒ Β βββ discord-bot.ts
|
|
86
|
+
βΒ Β βΒ Β βββ discord-utils.ts
|
|
87
|
+
βΒ Β βΒ Β βββ errors.ts
|
|
88
|
+
βΒ Β βΒ Β βββ escape-backticks.test.ts
|
|
89
|
+
βΒ Β βΒ Β βββ format-tables.test.ts
|
|
90
|
+
βΒ Β βΒ Β βββ format-tables.ts
|
|
91
|
+
βΒ Β βΒ Β βββ genai-worker-wrapper.ts
|
|
92
|
+
βΒ Β βΒ Β βββ genai-worker.ts
|
|
93
|
+
βΒ Β βΒ Β βββ genai.ts
|
|
94
|
+
βΒ Β βΒ Β βββ interaction-handler.ts
|
|
95
|
+
βΒ Β βΒ Β βββ limit-heading-depth.test.ts
|
|
96
|
+
βΒ Β βΒ Β βββ limit-heading-depth.ts
|
|
97
|
+
βΒ Β βΒ Β βββ logger.ts
|
|
98
|
+
βΒ Β βΒ Β βββ markdown.test.ts
|
|
99
|
+
βΒ Β βΒ Β βββ markdown.ts
|
|
100
|
+
βΒ Β βΒ Β βββ message-formatting.test.ts
|
|
101
|
+
βΒ Β βΒ Β βββ message-formatting.ts
|
|
102
|
+
βΒ Β βΒ Β βββ openai-realtime.ts
|
|
103
|
+
βΒ Β βΒ Β βββ opencode.ts
|
|
104
|
+
βΒ Β βΒ Β βββ session-handler.ts
|
|
105
|
+
βΒ Β βΒ Β βββ system-message.ts
|
|
106
|
+
βΒ Β βΒ Β βββ tools.ts
|
|
107
|
+
βΒ Β βΒ Β βββ unnest-code-blocks.test.ts
|
|
108
|
+
βΒ Β βΒ Β βββ unnest-code-blocks.ts
|
|
109
|
+
βΒ Β βΒ Β βββ utils.ts
|
|
110
|
+
βΒ Β βΒ Β βββ voice-handler.ts
|
|
111
|
+
βΒ Β βΒ Β βββ voice.ts
|
|
112
|
+
βΒ Β βΒ Β βββ worker-types.ts
|
|
113
|
+
βΒ Β βΒ Β βββ worktree-utils.ts
|
|
114
|
+
βΒ Β βΒ Β βββ xml.test.ts
|
|
115
|
+
βΒ Β βΒ Β βββ xml.ts
|
|
116
|
+
βΒ Β βββ tsconfig.json
|
|
117
|
+
βββ docs
|
|
118
|
+
βΒ Β βββ voice-channel-analysis.md
|
|
119
|
+
βββ errore
|
|
120
|
+
βββ KIMAKI_AGENTS.md
|
|
121
|
+
βββ LICENSE
|
|
122
|
+
βββ package.json
|
|
123
|
+
βββ pnpm-lock.yaml
|
|
124
|
+
βββ pnpm-workspace.yaml
|
|
125
|
+
βββ README.md
|
|
126
|
+
βββ tsconfig.base.json
|
|
127
|
+
|
|
128
|
+
7 directories, 85 files
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
*Completed in 2.3s*
|
|
134
|
+
|
|
135
|
+
### π€ Assistant (gpt-5.2-codex)
|
|
136
|
+
|
|
137
|
+
**Started using openai/gpt-5.2-codex**
|
|
138
|
+
|
|
139
|
+
#### π οΈ Tool: grep
|
|
140
|
+
|
|
141
|
+
**Input:**
|
|
142
|
+
```yaml
|
|
143
|
+
pattern: permission
|
|
144
|
+
path: .
|
|
145
|
+
include: '*.ts'
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Output:**
|
|
150
|
+
```
|
|
151
|
+
Found 72 matches
|
|
152
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/session-handler.ts:
|
|
153
|
+
Line 3: // Handles streaming events, permissions, abort signals, and message queuing.
|
|
154
|
+
Line 33: import { showPermissionDropdown, cleanupPermissionContext } from './commands/permissions.js'
|
|
155
|
+
Line 42: // Track multiple pending permissions per thread (keyed by permission ID)
|
|
156
|
+
Line 43: // OpenCode handles blocking/sequencing - we just need to track all pending permissions
|
|
157
|
+
Line 47: Map<string, { permission: PermissionRequest; messageId: string; directory: string; contextHash: string }> // permissionId -> data
|
|
158
|
+
Line 265: // Auto-reject ALL pending permissions for this thread
|
|
159
|
+
Line 271: sessionLogger.log(`[PERMISSION] Auto-rejecting permission ${permId} due to new message`)
|
|
160
|
+
Line 273: sessionLogger.log(`[PERMISSION] OpenCode v2 client unavailable for permission ${permId}`)
|
|
161
|
+
Line 279: return clientV2.permission.reply({
|
|
162
|
+
Line 285: sessionLogger.log(`[PERMISSION] Failed to auto-reject permission ${permId}:`, rejectResult)
|
|
163
|
+
Line 296: `β οΈ ${rejectedCount} pending permission request${plural} auto-rejected due to new message`,
|
|
164
|
+
Line 758: const handlePermissionAsked = async (permission: PermissionRequest) => {
|
|
165
|
+
Line 759: if (permission.sessionID !== session.id) {
|
|
166
|
+
Line 761: `[PERMISSION IGNORED] Permission for different session (expected: ${session.id}, got: ${permission.sessionID})`,
|
|
167
|
+
Line 767: if (threadPermissions?.has(permission.id)) {
|
|
168
|
+
Line 769: `[PERMISSION] Skipping duplicate permission ${permission.id} (already pending)`,
|
|
169
|
+
Line 775: `Permission requested: permission=${permission.permission}, patterns=${permission.patterns.join(', ')}`,
|
|
170
|
+
Line 785: permission,
|
|
171
|
+
Line 792: pendingPermissions.get(thread.id)!.set(permission.id, {
|
|
172
|
+
Line 793: permission,
|
|
173
|
+
Line 931: case 'permission.asked':
|
|
174
|
+
Line 934: case 'permission.replied':
|
|
175
|
+
|
|
176
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/cli.ts:
|
|
177
|
+
Line 776: 'Kimaki bot permission role - assign to users who can start sessions, send messages in threads, and use voice features',
|
|
178
|
+
|
|
179
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/discord-bot.ts:
|
|
180
|
+
Line 182: const isAdmin = message.member.permissions.has(PermissionsBitField.Flags.Administrator)
|
|
181
|
+
Line 183: const canManageServer = message.member.permissions.has(
|
|
182
|
+
Line 192: content: `You don't have permission to start sessions.\nTo use Kimaki, ask a server admin to give you the **Kimaki** role.`,
|
|
183
|
+
|
|
184
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/interaction-handler.ts:
|
|
185
|
+
Line 20: import { handlePermissionSelectMenu } from './commands/permissions.js'
|
|
186
|
+
Line 203: if (customId.startsWith('permission:')) {
|
|
187
|
+
|
|
188
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/system-message.ts:
|
|
189
|
+
Line 3: // including Discord-specific formatting rules, diff commands, and permissions info.
|
|
190
|
+
Line 30: ## permissions
|
|
191
|
+
Line 32: Only users with these Discord permissions can send messages to the bot:
|
|
192
|
+
Line 34: - Administrator permission
|
|
193
|
+
Line 35: - Manage Server permission
|
|
194
|
+
|
|
195
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/voice-handler.ts:
|
|
196
|
+
Line 565: const isAdmin = member.permissions.has(PermissionsBitField.Flags.Administrator)
|
|
197
|
+
Line 566: const canManageServer = member.permissions.has(PermissionsBitField.Flags.ManageGuild)
|
|
198
|
+
Line 589: m.permissions.has(PermissionsBitField.Flags.Administrator) ||
|
|
199
|
+
Line 590: m.permissions.has(PermissionsBitField.Flags.ManageGuild) ||
|
|
200
|
+
Line 627: m.permissions.has(PermissionsBitField.Flags.Administrator) ||
|
|
201
|
+
Line 628: m.permissions.has(PermissionsBitField.Flags.ManageGuild) ||
|
|
202
|
+
|
|
203
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/opencode.ts:
|
|
204
|
+
Line 128: permission: {
|
|
205
|
+
|
|
206
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/utils.ts:
|
|
207
|
+
Line 10: permissions?: bigint[]
|
|
208
|
+
Line 18: permissions = [
|
|
209
|
+
Line 38: const permissionsBitField = new PermissionsBitField(permissions)
|
|
210
|
+
Line 39: const permissionsValue = permissionsBitField.bitfield.toString()
|
|
211
|
+
Line 43: url.searchParams.set('permissions', permissionsValue)
|
|
212
|
+
|
|
213
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/permissions.ts:
|
|
214
|
+
Line 1: // Permission dropdown handler - Shows dropdown for permission requests.
|
|
215
|
+
Line 2: // When OpenCode asks for permission, this module renders a dropdown
|
|
216
|
+
Line 20: permission: PermissionRequest
|
|
217
|
+
Line 26: // Store pending permission contexts by hash
|
|
218
|
+
Line 30: * Show permission dropdown for a permission request.
|
|
219
|
+
Line 35: permission,
|
|
220
|
+
Line 39: permission: PermissionRequest
|
|
221
|
+
Line 45: permission,
|
|
222
|
+
Line 53: const patternStr = permission.patterns.join(', ')
|
|
223
|
+
Line 70: description: 'Reject this permission request',
|
|
224
|
+
Line 75: .setCustomId(`permission:${contextHash}`)
|
|
225
|
+
Line 81: const permissionMessage = await thread.send({
|
|
226
|
+
Line 84: `**Type:** \`${permission.permission}\`\n` +
|
|
227
|
+
Line 90: logger.log(`Showed permission dropdown for ${permission.id}`)
|
|
228
|
+
Line 92: return { messageId: permissionMessage.id, contextHash }
|
|
229
|
+
Line 96: * Handle dropdown selection for permission.
|
|
230
|
+
Line 103: if (!customId.startsWith('permission:')) {
|
|
231
|
+
Line 107: const contextHash = customId.replace('permission:', '')
|
|
232
|
+
Line 112: content: 'This permission request has expired or was already handled.',
|
|
233
|
+
Line 127: await clientV2.permission.reply({
|
|
234
|
+
Line 128: requestID: context.permission.id,
|
|
235
|
+
Line 146: const patternStr = context.permission.patterns.join(', ')
|
|
236
|
+
Line 150: `**Type:** \`${context.permission.permission}\`\n` +
|
|
237
|
+
Line 156: logger.log(`Permission ${context.permission.id} ${response}`)
|
|
238
|
+
Line 158: logger.error('Error handling permission:', error)
|
|
239
|
+
Line 160: content: `Failed to process permission: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
240
|
+
Line 167: * Clean up a pending permission context (e.g., on auto-reject).
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
#### π οΈ Tool: grep
|
|
244
|
+
|
|
245
|
+
**Input:**
|
|
246
|
+
```yaml
|
|
247
|
+
pattern: dropdown
|
|
248
|
+
path: .
|
|
249
|
+
include: '*.ts'
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Output:**
|
|
254
|
+
```
|
|
255
|
+
Found 19 matches
|
|
256
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/ask-question.ts:
|
|
257
|
+
Line 1: // AskUserQuestion tool handler - Shows Discord dropdowns for AI questions.
|
|
258
|
+
Line 2: // When the AI uses the AskUserQuestion tool, this module renders dropdowns
|
|
259
|
+
Line 47: * Show dropdown menus for question tool input.
|
|
260
|
+
Line 48: * Sends one message per question with the dropdown directly under the question text.
|
|
261
|
+
Line 79: // Send one message per question with its dropdown directly underneath
|
|
262
|
+
Line 119: logger.log(`Showed ${input.questions.length} question dropdown(s) for session ${sessionId}`)
|
|
263
|
+
Line 123: * Handle dropdown selection for AskUserQuestion.
|
|
264
|
+
Line 181: // Update this question's message: show answer and remove dropdown
|
|
265
|
+
Line 185: components: [], // Remove the dropdown
|
|
266
|
+
|
|
267
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/message-formatting.ts:
|
|
268
|
+
Line 307: // Question tool is handled via Discord dropdowns, not text
|
|
269
|
+
|
|
270
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/agent.ts:
|
|
271
|
+
Line 277: * These instantly switch to the specified agent without showing a dropdown.
|
|
272
|
+
|
|
273
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/permissions.ts:
|
|
274
|
+
Line 1: // Permission dropdown handler - Shows dropdown for permission requests.
|
|
275
|
+
Line 2: // When OpenCode asks for permission, this module renders a dropdown
|
|
276
|
+
Line 30: * Show permission dropdown for a permission request.
|
|
277
|
+
Line 55: // Build dropdown options
|
|
278
|
+
Line 90: logger.log(`Showed permission dropdown for ${permission.id}`)
|
|
279
|
+
Line 96: * Handle dropdown selection for permission.
|
|
280
|
+
Line 134: // Update message: show result and remove dropdown
|
|
281
|
+
Line 153: components: [], // Remove the dropdown
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
#### π οΈ Tool: grep
|
|
285
|
+
|
|
286
|
+
**Input:**
|
|
287
|
+
```yaml
|
|
288
|
+
pattern: opencode
|
|
289
|
+
path: .
|
|
290
|
+
include: '*.ts'
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Output:**
|
|
295
|
+
```
|
|
296
|
+
Found 97 matches
|
|
297
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/session-handler.ts:
|
|
298
|
+
Line 5: import type { Part, PermissionRequest, QuestionRequest } from '@opencode-ai/sdk/v2'
|
|
299
|
+
Line 6: import type { FilePartInput } from '@opencode-ai/sdk'
|
|
300
|
+
Line 22: } from './opencode.js'
|
|
301
|
+
Line 742: await sendThreadMessage(thread, `β opencode session error: ${errorMessage}`)
|
|
302
|
+
|
|
303
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/user-command.ts:
|
|
304
|
+
Line 2: // Handles slash commands that map to user-configured commands in opencode.json.
|
|
305
|
+
|
|
306
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/worktree.ts:
|
|
307
|
+
Line 14: import { initializeOpencodeForDirectory, getOpencodeClientV2 } from '../opencode.js'
|
|
308
|
+
Line 31: * Format worktree name: lowercase, spaces to dashes, remove special chars, add opencode/kimaki- prefix.
|
|
309
|
+
Line 32: * "My Feature" β "opencode/kimaki-my-feature"
|
|
310
|
+
Line 41: return `opencode/kimaki-${formatted}`
|
|
311
|
+
Line 138: // Initialize opencode and check if worktree already exists
|
|
312
|
+
Line 152: // SDK returns array of directory paths like "~/.opencode/worktree/abc/kimaki-my-feature"
|
|
313
|
+
|
|
314
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/resume.ts:
|
|
315
|
+
Line 12: import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
316
|
+
|
|
317
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/session.ts:
|
|
318
|
+
Line 8: import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
319
|
+
|
|
320
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/cli.ts:
|
|
321
|
+
Line 4: // project channel creation, and launching the bot with opencode integration.
|
|
322
|
+
Line 31: import type { OpencodeClient, Command as OpencodeCommand } from '@opencode-ai/sdk'
|
|
323
|
+
Line 353: // Sanitize command name: oh-my-opencode uses MCP commands with colons, which Discord doesn't allow
|
|
324
|
+
Line 501: const opencodeResult = await initializeOpencodeForDirectory(currentDir)
|
|
325
|
+
Line 502: if (opencodeResult instanceof Error) {
|
|
326
|
+
Line 503: cliLogger.warn('Background OpenCode init failed:', opencodeResult.message)
|
|
327
|
+
Line 509: const getClient = opencodeResult
|
|
328
|
+
Line 538: const opencodeCheck = spawnSync('which', ['opencode'], { shell: true })
|
|
329
|
+
Line 540: if (opencodeCheck.status !== 0) {
|
|
330
|
+
Line 556: execSync('curl -fsSL https://opencode.ai/install | bash', {
|
|
331
|
+
Line 562: // The install script adds opencode to PATH via shell configuration
|
|
332
|
+
Line 565: `${process.env.HOME}/.local/bin/opencode`,
|
|
333
|
+
Line 566: `${process.env.HOME}/.opencode/bin/opencode`,
|
|
334
|
+
Line 567: '/usr/local/bin/opencode',
|
|
335
|
+
Line 568: '/opt/opencode/bin/opencode',
|
|
336
|
+
Line 737: const opencodePromise = initializeOpencodeForDirectory(currentDir).then((result) => {
|
|
337
|
+
Line 857: const getClient = await opencodePromise
|
|
338
|
+
Line 897: if (path.basename(project.worktree).startsWith('opencode-test-')) {
|
|
339
|
+
|
|
340
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/discord-bot.ts:
|
|
341
|
+
Line 15: import { initializeOpencodeForDirectory, getOpencodeServers, getOpencodeClientV2 } from './opencode.js'
|
|
342
|
+
Line 44: export { initializeOpencodeForDirectory } from './opencode.js'
|
|
343
|
+
|
|
344
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/worktree-utils.ts:
|
|
345
|
+
Line 7: import type { getOpencodeClientV2 } from './opencode.js'
|
|
346
|
+
|
|
347
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/system-message.ts:
|
|
348
|
+
Line 8: /** The branch name (e.g., opencode/kimaki-feature) */
|
|
349
|
+
|
|
350
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/voice-handler.ts:
|
|
351
|
+
Line 196: Your job is to manage many opencode agent chat instances. Opencode is the agent used to write the code, it is similar to Claude Code.
|
|
352
|
+
Line 198: For everything the user asks it is implicit that the user is asking for you to proxy the requests to opencode sessions.
|
|
353
|
+
|
|
354
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/ask-question.ts:
|
|
355
|
+
Line 13: import { getOpencodeClientV2 } from '../opencode.js'
|
|
356
|
+
|
|
357
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/opencode.ts:
|
|
358
|
+
Line 9: import { createOpencodeClient, type OpencodeClient, type Config } from '@opencode-ai/sdk'
|
|
359
|
+
Line 13: } from '@opencode-ai/sdk/v2'
|
|
360
|
+
Line 24: const opencodeLogger = createLogger('OPENCODE')
|
|
361
|
+
Line 26: const opencodeServers = new Map<
|
|
362
|
+
Line 71: opencodeLogger.debug(`Server polling attempt failed: ${response.message}`)
|
|
363
|
+
Line 89: const existing = opencodeServers.get(directory)
|
|
364
|
+
Line 91: opencodeLogger.log(
|
|
365
|
+
Line 95: const entry = opencodeServers.get(directory)
|
|
366
|
+
Line 116: const opencodeCommand = process.env.OPENCODE_PATH || 'opencode'
|
|
367
|
+
Line 118: const serverProcess = spawn(opencodeCommand, ['serve', '--port', port.toString()], {
|
|
368
|
+
Line 125: $schema: 'https://opencode.ai/config.json',
|
|
369
|
+
Line 141: `Spawned opencode serve --port ${port} in ${directory} (pid: ${serverProcess.pid})`,
|
|
370
|
+
Line 157: opencodeLogger.log(`Opencode server on ${directory} exited with code:`, code)
|
|
371
|
+
Line 158: opencodeServers.delete(directory)
|
|
372
|
+
Line 163: opencodeLogger.log(
|
|
373
|
+
Line 168: opencodeLogger.error(`Failed to restart opencode server:`, result)
|
|
374
|
+
Line 172: opencodeLogger.error(`Server for ${directory} crashed too many times (5), not restarting`)
|
|
375
|
+
Line 182: opencodeLogger.error(`Server failed to start for ${directory}:`)
|
|
376
|
+
Line 184: opencodeLogger.error(` ${line}`)
|
|
377
|
+
Line 188: opencodeLogger.log(`Server ready on port ${port}`)
|
|
378
|
+
Line 207: opencodeServers.set(directory, {
|
|
379
|
+
Line 215: const entry = opencodeServers.get(directory)
|
|
380
|
+
Line 224: return opencodeServers
|
|
381
|
+
Line 228: const entry = opencodeServers.get(directory)
|
|
382
|
+
Line 233: const entry = opencodeServers.get(directory)
|
|
383
|
+
|
|
384
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.ts:
|
|
385
|
+
Line 6: import type { OpencodeClient } from '@opencode-ai/sdk'
|
|
386
|
+
|
|
387
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/message-formatting.ts:
|
|
388
|
+
Line 5: import type { Part } from '@opencode-ai/sdk/v2'
|
|
389
|
+
Line 6: import type { FilePartInput } from '@opencode-ai/sdk'
|
|
390
|
+
|
|
391
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/share.ts:
|
|
392
|
+
Line 6: import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
393
|
+
|
|
394
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/abort.ts:
|
|
395
|
+
Line 6: import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
396
|
+
|
|
397
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/model.ts:
|
|
398
|
+
Line 14: import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
399
|
+
|
|
400
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/fork.ts:
|
|
401
|
+
Line 13: import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
402
|
+
|
|
403
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/undo-redo.ts:
|
|
404
|
+
Line 6: import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
405
|
+
|
|
406
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/add-project.ts:
|
|
407
|
+
Line 7: import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
408
|
+
Line 118: if (path.basename(project.worktree).startsWith('opencode-test-')) {
|
|
409
|
+
|
|
410
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/tools.ts:
|
|
411
|
+
Line 14: } from '@opencode-ai/sdk'
|
|
412
|
+
Line 193: toolsLogger.log(`Listing opencode sessions`)
|
|
413
|
+
|
|
414
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/agent.ts:
|
|
415
|
+
Line 15: import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
416
|
+
|
|
417
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/markdown.test.ts:
|
|
418
|
+
Line 3: import { OpencodeClient } from '@opencode-ai/sdk'
|
|
419
|
+
Line 14: // Try different endpoints that opencode might expose
|
|
420
|
+
Line 43: // Use default opencode port
|
|
421
|
+
Line 46: // Spawn opencode server
|
|
422
|
+
Line 47: console.log(`Starting opencode server on port ${port}...`)
|
|
423
|
+
Line 48: serverProcess = spawn('opencode', ['serve', '--port', port.toString()], {
|
|
424
|
+
|
|
425
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/permissions.ts:
|
|
426
|
+
Line 12: import type { PermissionRequest } from '@opencode-ai/sdk/v2'
|
|
427
|
+
Line 13: import { getOpencodeClientV2 } from '../opencode.js'
|
|
428
|
+
|
|
429
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/src/message-formatting.test.ts:
|
|
430
|
+
Line 3: import type { Part } from '@opencode-ai/sdk'
|
|
431
|
+
|
|
432
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/scripts/test-model-id.ts:
|
|
433
|
+
Line 12: import { createOpencodeClient } from '@opencode-ai/sdk'
|
|
434
|
+
Line 56: console.log(`Starting opencode server on port ${port}...`)
|
|
435
|
+
Line 58: const serverProcess = spawn('opencode', ['serve', '--port', port.toString()], {
|
|
436
|
+
Line 64: console.log(`[opencode] ${data.toString().trim()}`)
|
|
437
|
+
Line 68: console.error(`[opencode] ${data.toString().trim()}`)
|
|
438
|
+
|
|
439
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/scripts/list-projects.ts:
|
|
440
|
+
Line 2: import { createOpencodeClient } from '@opencode-ai/sdk'
|
|
441
|
+
Line 12: '(Make sure OpenCode is running with: opencode internal-server)\n',
|
|
442
|
+
|
|
443
|
+
/Users/morse/Documents/GitHub/kimakivoice/discord/scripts/get-last-session-messages.ts:
|
|
444
|
+
Line 2: import { createOpencodeClient } from '@opencode-ai/sdk'
|
|
445
|
+
Line 61: const opencodeCommand = process.env.OPENCODE_PATH || 'opencode'
|
|
446
|
+
Line 66: opencodeCommand,
|
|
447
|
+
Line 80: console.log(`[opencode]: ${data.toString().trim()}`)
|
|
448
|
+
Line 84: console.error(`[opencode error]: ${data.toString().trim()}`)
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
*Completed in 3.1s*
|
|
453
|
+
|
|
454
|
+
### π€ Assistant (gpt-5.2-codex)
|
|
455
|
+
|
|
456
|
+
**Started using openai/gpt-5.2-codex**
|
|
457
|
+
|
|
458
|
+
#### π οΈ Tool: read
|
|
459
|
+
|
|
460
|
+
**Input:**
|
|
461
|
+
```yaml
|
|
462
|
+
filePath: /Users/morse/Documents/GitHub/kimakivoice/discord/src/commands/permissions.ts
|
|
463
|
+
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Output:**
|
|
467
|
+
```
|
|
468
|
+
<file>
|
|
469
|
+
00001| // Permission dropdown handler - Shows dropdown for permission requests.
|
|
470
|
+
00002| // When OpenCode asks for permission, this module renders a dropdown
|
|
471
|
+
00003| // with Accept, Accept Always, and Deny options.
|
|
472
|
+
00004|
|
|
473
|
+
00005| import {
|
|
474
|
+
00006| StringSelectMenuBuilder,
|
|
475
|
+
00007| StringSelectMenuInteraction,
|
|
476
|
+
00008| ActionRowBuilder,
|
|
477
|
+
00009| type ThreadChannel,
|
|
478
|
+
00010| } from 'discord.js'
|
|
479
|
+
00011| import crypto from 'node:crypto'
|
|
480
|
+
00012| import type { PermissionRequest } from '@opencode-ai/sdk/v2'
|
|
481
|
+
00013| import { getOpencodeClientV2 } from '../opencode.js'
|
|
482
|
+
00014| import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
483
|
+
00015| import { createLogger } from '../logger.js'
|
|
484
|
+
00016|
|
|
485
|
+
00017| const logger = createLogger('PERMISSIONS')
|
|
486
|
+
00018|
|
|
487
|
+
00019| type PendingPermissionContext = {
|
|
488
|
+
00020| permission: PermissionRequest
|
|
489
|
+
00021| directory: string
|
|
490
|
+
00022| thread: ThreadChannel
|
|
491
|
+
00023| contextHash: string
|
|
492
|
+
00024| }
|
|
493
|
+
00025|
|
|
494
|
+
00026| // Store pending permission contexts by hash
|
|
495
|
+
00027| export const pendingPermissionContexts = new Map<string, PendingPermissionContext>()
|
|
496
|
+
00028|
|
|
497
|
+
00029| /**
|
|
498
|
+
00030| * Show permission dropdown for a permission request.
|
|
499
|
+
00031| * Returns the message ID and context hash for tracking.
|
|
500
|
+
00032| */
|
|
501
|
+
00033| export async function showPermissionDropdown({
|
|
502
|
+
00034| thread,
|
|
503
|
+
00035| permission,
|
|
504
|
+
00036| directory,
|
|
505
|
+
00037| }: {
|
|
506
|
+
00038| thread: ThreadChannel
|
|
507
|
+
00039| permission: PermissionRequest
|
|
508
|
+
00040| directory: string
|
|
509
|
+
00041| }): Promise<{ messageId: string; contextHash: string }> {
|
|
510
|
+
00042| const contextHash = crypto.randomBytes(8).toString('hex')
|
|
511
|
+
00043|
|
|
512
|
+
00044| const context: PendingPermissionContext = {
|
|
513
|
+
00045| permission,
|
|
514
|
+
00046| directory,
|
|
515
|
+
00047| thread,
|
|
516
|
+
00048| contextHash,
|
|
517
|
+
00049| }
|
|
518
|
+
00050|
|
|
519
|
+
00051| pendingPermissionContexts.set(contextHash, context)
|
|
520
|
+
00052|
|
|
521
|
+
00053| const patternStr = permission.patterns.join(', ')
|
|
522
|
+
00054|
|
|
523
|
+
00055| // Build dropdown options
|
|
524
|
+
00056| const options = [
|
|
525
|
+
00057| {
|
|
526
|
+
00058| label: 'Accept',
|
|
527
|
+
00059| value: 'once',
|
|
528
|
+
00060| description: 'Allow this request only',
|
|
529
|
+
00061| },
|
|
530
|
+
00062| {
|
|
531
|
+
00063| label: 'Accept Always',
|
|
532
|
+
00064| value: 'always',
|
|
533
|
+
00065| description: 'Auto-approve similar requests',
|
|
534
|
+
00066| },
|
|
535
|
+
00067| {
|
|
536
|
+
00068| label: 'Deny',
|
|
537
|
+
00069| value: 'reject',
|
|
538
|
+
00070| description: 'Reject this permission request',
|
|
539
|
+
00071| },
|
|
540
|
+
00072| ]
|
|
541
|
+
00073|
|
|
542
|
+
00074| const selectMenu = new StringSelectMenuBuilder()
|
|
543
|
+
00075| .setCustomId(`permission:${contextHash}`)
|
|
544
|
+
00076| .setPlaceholder('Choose an action')
|
|
545
|
+
00077| .addOptions(options)
|
|
546
|
+
00078|
|
|
547
|
+
00079| const actionRow = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
|
|
548
|
+
00080|
|
|
549
|
+
00081| const permissionMessage = await thread.send({
|
|
550
|
+
00082| content:
|
|
551
|
+
00083| `β οΈ **Permission Required**\n\n` +
|
|
552
|
+
00084| `**Type:** \`${permission.permission}\`\n` +
|
|
553
|
+
00085| (patternStr ? `**Pattern:** \`${patternStr}\`` : ''),
|
|
554
|
+
00086| components: [actionRow],
|
|
555
|
+
00087| flags: NOTIFY_MESSAGE_FLAGS,
|
|
556
|
+
00088| })
|
|
557
|
+
00089|
|
|
558
|
+
00090| logger.log(`Showed permission dropdown for ${permission.id}`)
|
|
559
|
+
00091|
|
|
560
|
+
00092| return { messageId: permissionMessage.id, contextHash }
|
|
561
|
+
00093| }
|
|
562
|
+
00094|
|
|
563
|
+
00095| /**
|
|
564
|
+
00096| * Handle dropdown selection for permission.
|
|
565
|
+
00097| */
|
|
566
|
+
00098| export async function handlePermissionSelectMenu(
|
|
567
|
+
00099| interaction: StringSelectMenuInteraction,
|
|
568
|
+
00100| ): Promise<void> {
|
|
569
|
+
00101| const customId = interaction.customId
|
|
570
|
+
00102|
|
|
571
|
+
00103| if (!customId.startsWith('permission:')) {
|
|
572
|
+
00104| return
|
|
573
|
+
00105| }
|
|
574
|
+
00106|
|
|
575
|
+
00107| const contextHash = customId.replace('permission:', '')
|
|
576
|
+
00108| const context = pendingPermissionContexts.get(contextHash)
|
|
577
|
+
00109|
|
|
578
|
+
00110| if (!context) {
|
|
579
|
+
00111| await interaction.reply({
|
|
580
|
+
00112| content: 'This permission request has expired or was already handled.',
|
|
581
|
+
00113| ephemeral: true,
|
|
582
|
+
00114| })
|
|
583
|
+
00115| return
|
|
584
|
+
00116| }
|
|
585
|
+
00117|
|
|
586
|
+
00118| await interaction.deferUpdate()
|
|
587
|
+
00119|
|
|
588
|
+
00120| const response = interaction.values[0] as 'once' | 'always' | 'reject'
|
|
589
|
+
00121|
|
|
590
|
+
00122| try {
|
|
591
|
+
00123| const clientV2 = getOpencodeClientV2(context.directory)
|
|
592
|
+
00124| if (!clientV2) {
|
|
593
|
+
00125| throw new Error('OpenCode server not found for directory')
|
|
594
|
+
00126| }
|
|
595
|
+
00127| await clientV2.permission.reply({
|
|
596
|
+
00128| requestID: context.permission.id,
|
|
597
|
+
00129| reply: response,
|
|
598
|
+
00130| })
|
|
599
|
+
00131|
|
|
600
|
+
00132| pendingPermissionContexts.delete(contextHash)
|
|
601
|
+
00133|
|
|
602
|
+
00134| // Update message: show result and remove dropdown
|
|
603
|
+
00135| const resultText = (() => {
|
|
604
|
+
00136| switch (response) {
|
|
605
|
+
00137| case 'once':
|
|
606
|
+
00138| return 'β
Permission **accepted**'
|
|
607
|
+
00139| case 'always':
|
|
608
|
+
00140| return 'β
Permission **accepted** (auto-approve similar requests)'
|
|
609
|
+
00141| case 'reject':
|
|
610
|
+
00142| return 'β Permission **rejected**'
|
|
611
|
+
00143| }
|
|
612
|
+
00144| })()
|
|
613
|
+
00145|
|
|
614
|
+
00146| const patternStr = context.permission.patterns.join(', ')
|
|
615
|
+
00147| await interaction.editReply({
|
|
616
|
+
00148| content:
|
|
617
|
+
00149| `β οΈ **Permission Required**\n\n` +
|
|
618
|
+
00150| `**Type:** \`${context.permission.permission}\`\n` +
|
|
619
|
+
00151| (patternStr ? `**Pattern:** \`${patternStr}\`\n\n` : '\n') +
|
|
620
|
+
00152| resultText,
|
|
621
|
+
00153| components: [], // Remove the dropdown
|
|
622
|
+
00154| })
|
|
623
|
+
00155|
|
|
624
|
+
00156| logger.log(`Permission ${context.permission.id} ${response}`)
|
|
625
|
+
00157| } catch (error) {
|
|
626
|
+
00158| logger.error('Error handling permission:', error)
|
|
627
|
+
00159| await interaction.editReply({
|
|
628
|
+
00160| content: `Failed to process permission: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
629
|
+
00161| components: [],
|
|
630
|
+
00162| })
|
|
631
|
+
00163| }
|
|
632
|
+
00164| }
|
|
633
|
+
00165|
|
|
634
|
+
00166| /**
|
|
635
|
+
00167| * Clean up a pending permission context (e.g., on auto-reject).
|
|
636
|
+
00168| */
|
|
637
|
+
00169| export function cleanupPermissionContext(contextHash: string): void {
|
|
638
|
+
00170| pendingPermissionContexts.delete(contextHash)
|
|
639
|
+
00171| }
|
|
640
|
+
00172|
|
|
641
|
+
|
|
642
|
+
(End of file - total 172 lines)
|
|
643
|
+
</file>
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
#### π οΈ Tool: read
|
|
647
|
+
|
|
648
|
+
**Input:**
|
|
649
|
+
```yaml
|
|
650
|
+
filePath: /Users/morse/Documents/GitHub/kimakivoice/discord/src/session-handler.ts
|
|
651
|
+
offset: 700
|
|
652
|
+
limit: 260
|
|
653
|
+
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
**Output:**
|
|
657
|
+
```
|
|
658
|
+
<file>
|
|
659
|
+
00701| getDatabase()
|
|
660
|
+
00702| .prepare(
|
|
661
|
+
00703| 'INSERT OR REPLACE INTO part_messages (part_id, message_id, thread_id) VALUES (?, ?, ?)',
|
|
662
|
+
00704| )
|
|
663
|
+
00705| .run(part.id, sendResult.id, thread.id)
|
|
664
|
+
00706| }
|
|
665
|
+
00707|
|
|
666
|
+
00708| const handlePartUpdated = async (part: Part) => {
|
|
667
|
+
00709| storePart(part)
|
|
668
|
+
00710|
|
|
669
|
+
00711| const subtaskInfo = subtaskSessions.get(part.sessionID)
|
|
670
|
+
00712| const isSubtaskEvent = Boolean(subtaskInfo)
|
|
671
|
+
00713|
|
|
672
|
+
00714| if (part.sessionID !== session.id && !isSubtaskEvent) {
|
|
673
|
+
00715| return
|
|
674
|
+
00716| }
|
|
675
|
+
00717|
|
|
676
|
+
00718| if (isSubtaskEvent && subtaskInfo) {
|
|
677
|
+
00719| await handleSubtaskPart(part, subtaskInfo)
|
|
678
|
+
00720| return
|
|
679
|
+
00721| }
|
|
680
|
+
00722|
|
|
681
|
+
00723| await handleMainPart(part)
|
|
682
|
+
00724| }
|
|
683
|
+
00725|
|
|
684
|
+
00726| const handleSessionError = async ({
|
|
685
|
+
00727| sessionID,
|
|
686
|
+
00728| error,
|
|
687
|
+
00729| }: {
|
|
688
|
+
00730| sessionID?: string
|
|
689
|
+
00731| error?: { data?: { message?: string } }
|
|
690
|
+
00732| }) => {
|
|
691
|
+
00733| if (!sessionID || sessionID !== session.id) {
|
|
692
|
+
00734| voiceLogger.log(
|
|
693
|
+
00735| `[SESSION ERROR IGNORED] Error for different session (expected: ${session.id}, got: ${sessionID})`,
|
|
694
|
+
00736| )
|
|
695
|
+
00737| return
|
|
696
|
+
00738| }
|
|
697
|
+
00739|
|
|
698
|
+
00740| const errorMessage = error?.data?.message || 'Unknown error'
|
|
699
|
+
00741| sessionLogger.error(`Sending error to thread: ${errorMessage}`)
|
|
700
|
+
00742| await sendThreadMessage(thread, `β opencode session error: ${errorMessage}`)
|
|
701
|
+
00743|
|
|
702
|
+
00744| if (!originalMessage) {
|
|
703
|
+
00745| return
|
|
704
|
+
00746| }
|
|
705
|
+
00747| const reactionResult = await errore.tryAsync(async () => {
|
|
706
|
+
00748| await originalMessage.reactions.removeAll()
|
|
707
|
+
00749| await originalMessage.react('β')
|
|
708
|
+
00750| })
|
|
709
|
+
00751| if (reactionResult instanceof Error) {
|
|
710
|
+
00752| discordLogger.log(`Could not update reaction:`, reactionResult)
|
|
711
|
+
00753| } else {
|
|
712
|
+
00754| voiceLogger.log(`[REACTION] Added error reaction due to session error`)
|
|
713
|
+
00755| }
|
|
714
|
+
00756| }
|
|
715
|
+
00757|
|
|
716
|
+
00758| const handlePermissionAsked = async (permission: PermissionRequest) => {
|
|
717
|
+
00759| if (permission.sessionID !== session.id) {
|
|
718
|
+
00760| voiceLogger.log(
|
|
719
|
+
00761| `[PERMISSION IGNORED] Permission for different session (expected: ${session.id}, got: ${permission.sessionID})`,
|
|
720
|
+
00762| )
|
|
721
|
+
00763| return
|
|
722
|
+
00764| }
|
|
723
|
+
00765|
|
|
724
|
+
00766| const threadPermissions = pendingPermissions.get(thread.id)
|
|
725
|
+
00767| if (threadPermissions?.has(permission.id)) {
|
|
726
|
+
00768| sessionLogger.log(
|
|
727
|
+
00769| `[PERMISSION] Skipping duplicate permission ${permission.id} (already pending)`,
|
|
728
|
+
00770| )
|
|
729
|
+
00771| return
|
|
730
|
+
00772| }
|
|
731
|
+
00773|
|
|
732
|
+
00774| sessionLogger.log(
|
|
733
|
+
00775| `Permission requested: permission=${permission.permission}, patterns=${permission.patterns.join(', ')}`,
|
|
734
|
+
00776| )
|
|
735
|
+
00777|
|
|
736
|
+
00778| if (stopTyping) {
|
|
737
|
+
00779| stopTyping()
|
|
738
|
+
00780| stopTyping = null
|
|
739
|
+
00781| }
|
|
740
|
+
00782|
|
|
741
|
+
00783| const { messageId, contextHash } = await showPermissionDropdown({
|
|
742
|
+
00784| thread,
|
|
743
|
+
00785| permission,
|
|
744
|
+
00786| directory,
|
|
745
|
+
00787| })
|
|
746
|
+
00788|
|
|
747
|
+
00789| if (!pendingPermissions.has(thread.id)) {
|
|
748
|
+
00790| pendingPermissions.set(thread.id, new Map())
|
|
749
|
+
00791| }
|
|
750
|
+
00792| pendingPermissions.get(thread.id)!.set(permission.id, {
|
|
751
|
+
00793| permission,
|
|
752
|
+
00794| messageId,
|
|
753
|
+
00795| directory,
|
|
754
|
+
00796| contextHash,
|
|
755
|
+
00797| })
|
|
756
|
+
00798| }
|
|
757
|
+
00799|
|
|
758
|
+
00800| const handlePermissionReplied = ({
|
|
759
|
+
00801| requestID,
|
|
760
|
+
00802| reply,
|
|
761
|
+
00803| sessionID,
|
|
762
|
+
00804| }: {
|
|
763
|
+
00805| requestID: string
|
|
764
|
+
00806| reply: string
|
|
765
|
+
00807| sessionID: string
|
|
766
|
+
00808| }) => {
|
|
767
|
+
00809| if (sessionID !== session.id) {
|
|
768
|
+
00810| return
|
|
769
|
+
00811| }
|
|
770
|
+
00812|
|
|
771
|
+
00813| sessionLogger.log(`Permission ${requestID} replied with: ${reply}`)
|
|
772
|
+
00814|
|
|
773
|
+
00815| const threadPermissions = pendingPermissions.get(thread.id)
|
|
774
|
+
00816| if (!threadPermissions) {
|
|
775
|
+
00817| return
|
|
776
|
+
00818| }
|
|
777
|
+
00819| const pending = threadPermissions.get(requestID)
|
|
778
|
+
00820| if (!pending) {
|
|
779
|
+
00821| return
|
|
780
|
+
00822| }
|
|
781
|
+
00823| cleanupPermissionContext(pending.contextHash)
|
|
782
|
+
00824| threadPermissions.delete(requestID)
|
|
783
|
+
00825| if (threadPermissions.size === 0) {
|
|
784
|
+
00826| pendingPermissions.delete(thread.id)
|
|
785
|
+
00827| }
|
|
786
|
+
00828| }
|
|
787
|
+
00829|
|
|
788
|
+
00830| const handleQuestionAsked = async (questionRequest: QuestionRequest) => {
|
|
789
|
+
00831| if (questionRequest.sessionID !== session.id) {
|
|
790
|
+
00832| sessionLogger.log(
|
|
791
|
+
00833| `[QUESTION IGNORED] Question for different session (expected: ${session.id}, got: ${questionRequest.sessionID})`,
|
|
792
|
+
00834| )
|
|
793
|
+
00835| return
|
|
794
|
+
00836| }
|
|
795
|
+
00837|
|
|
796
|
+
00838| sessionLogger.log(
|
|
797
|
+
00839| `Question requested: id=${questionRequest.id}, questions=${questionRequest.questions.length}`,
|
|
798
|
+
00840| )
|
|
799
|
+
00841|
|
|
800
|
+
00842| if (stopTyping) {
|
|
801
|
+
00843| stopTyping()
|
|
802
|
+
00844| stopTyping = null
|
|
803
|
+
00845| }
|
|
804
|
+
00846|
|
|
805
|
+
00847| await flushBufferedParts({
|
|
806
|
+
00848| messageID: assistantMessageId || '',
|
|
807
|
+
00849| force: true,
|
|
808
|
+
00850| })
|
|
809
|
+
00851|
|
|
810
|
+
00852| await showAskUserQuestionDropdowns({
|
|
811
|
+
00853| thread,
|
|
812
|
+
00854| sessionId: session.id,
|
|
813
|
+
00855| directory,
|
|
814
|
+
00856| requestId: questionRequest.id,
|
|
815
|
+
00857| input: { questions: questionRequest.questions },
|
|
816
|
+
00858| })
|
|
817
|
+
00859|
|
|
818
|
+
00860| const queue = messageQueue.get(thread.id)
|
|
819
|
+
00861| if (!queue || queue.length === 0) {
|
|
820
|
+
00862| return
|
|
821
|
+
00863| }
|
|
822
|
+
00864|
|
|
823
|
+
00865| const nextMessage = queue.shift()!
|
|
824
|
+
00866| if (queue.length === 0) {
|
|
825
|
+
00867| messageQueue.delete(thread.id)
|
|
826
|
+
00868| }
|
|
827
|
+
00869|
|
|
828
|
+
00870| sessionLogger.log(
|
|
829
|
+
00871| `[QUEUE] Question shown but queue has messages, processing from ${nextMessage.username}`,
|
|
830
|
+
00872| )
|
|
831
|
+
00873|
|
|
832
|
+
00874| await sendThreadMessage(
|
|
833
|
+
00875| thread,
|
|
834
|
+
00876| `Β» **${nextMessage.username}:** ${nextMessage.prompt.slice(0, 150)}${nextMessage.prompt.length > 150 ? '...' : ''}`,
|
|
835
|
+
00877| )
|
|
836
|
+
00878|
|
|
837
|
+
00879| setImmediate(() => {
|
|
838
|
+
00880| void errore
|
|
839
|
+
00881| .tryAsync(async () => {
|
|
840
|
+
00882| return handleOpencodeSession({
|
|
841
|
+
00883| prompt: nextMessage.prompt,
|
|
842
|
+
00884| thread,
|
|
843
|
+
00885| projectDirectory: directory,
|
|
844
|
+
00886| images: nextMessage.images,
|
|
845
|
+
00887| channelId,
|
|
846
|
+
00888| })
|
|
847
|
+
00889| })
|
|
848
|
+
00890| .then(async (result) => {
|
|
849
|
+
00891| if (!(result instanceof Error)) {
|
|
850
|
+
00892| return
|
|
851
|
+
00893| }
|
|
852
|
+
00894| sessionLogger.error(`[QUEUE] Failed to process queued message:`, result)
|
|
853
|
+
00895| await sendThreadMessage(
|
|
854
|
+
00896| thread,
|
|
855
|
+
00897| `β Queued message failed: ${result.message.slice(0, 200)}`,
|
|
856
|
+
00898| )
|
|
857
|
+
00899| })
|
|
858
|
+
00900| })
|
|
859
|
+
00901| }
|
|
860
|
+
00902|
|
|
861
|
+
00903| const handleSessionIdle = (idleSessionId: string) => {
|
|
862
|
+
00904| if (idleSessionId === session.id) {
|
|
863
|
+
00905| sessionLogger.log(`[SESSION IDLE] Session ${session.id} is idle, aborting`)
|
|
864
|
+
00906| abortController.abort('finished')
|
|
865
|
+
00907| return
|
|
866
|
+
00908| }
|
|
867
|
+
00909|
|
|
868
|
+
00910| if (!subtaskSessions.has(idleSessionId)) {
|
|
869
|
+
00911| return
|
|
870
|
+
00912| }
|
|
871
|
+
00913| const subtask = subtaskSessions.get(idleSessionId)
|
|
872
|
+
00914| sessionLogger.log(`[SUBTASK IDLE] Subtask "${subtask?.label}" completed`)
|
|
873
|
+
00915| subtaskSessions.delete(idleSessionId)
|
|
874
|
+
00916| }
|
|
875
|
+
00917|
|
|
876
|
+
00918| try {
|
|
877
|
+
00919| for await (const event of events) {
|
|
878
|
+
00920| switch (event.type) {
|
|
879
|
+
00921| case 'message.updated':
|
|
880
|
+
00922| await handleMessageUpdated(event.properties.info)
|
|
881
|
+
00923| break
|
|
882
|
+
00924| case 'message.part.updated':
|
|
883
|
+
00925| await handlePartUpdated(event.properties.part)
|
|
884
|
+
00926| break
|
|
885
|
+
00927| case 'session.error':
|
|
886
|
+
00928| sessionLogger.error(`ERROR:`, event.properties)
|
|
887
|
+
00929| await handleSessionError(event.properties)
|
|
888
|
+
00930| break
|
|
889
|
+
00931| case 'permission.asked':
|
|
890
|
+
00932| await handlePermissionAsked(event.properties)
|
|
891
|
+
00933| break
|
|
892
|
+
00934| case 'permission.replied':
|
|
893
|
+
00935| handlePermissionReplied(event.properties)
|
|
894
|
+
00936| break
|
|
895
|
+
00937| case 'question.asked':
|
|
896
|
+
00938| await handleQuestionAsked(event.properties)
|
|
897
|
+
00939| break
|
|
898
|
+
00940| case 'session.idle':
|
|
899
|
+
00941| handleSessionIdle(event.properties.sessionID)
|
|
900
|
+
00942| break
|
|
901
|
+
00943| default:
|
|
902
|
+
00944| break
|
|
903
|
+
00945| }
|
|
904
|
+
00946| }
|
|
905
|
+
00947| } catch (e) {
|
|
906
|
+
00948| if (isAbortError(e, abortController.signal)) {
|
|
907
|
+
00949| sessionLogger.log('AbortController aborted event handling (normal exit)')
|
|
908
|
+
00950| return
|
|
909
|
+
00951| }
|
|
910
|
+
00952| sessionLogger.error(`Unexpected error in event handling code`, e)
|
|
911
|
+
00953| throw e
|
|
912
|
+
00954| } finally {
|
|
913
|
+
00955| const finalMessageId = assistantMessageId
|
|
914
|
+
00956| if (finalMessageId) {
|
|
915
|
+
00957| const parts = getBufferedParts(finalMessageId)
|
|
916
|
+
00958| for (const part of parts) {
|
|
917
|
+
00959| if (!sentPartIds.has(part.id)) {
|
|
918
|
+
00960| await sendPartMessage(part)
|
|
919
|
+
|
|
920
|
+
(File has more lines. Use 'offset' parameter to read beyond line 960)
|
|
921
|
+
</file>
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
#### π οΈ Tool: read
|
|
925
|
+
|
|
926
|
+
**Input:**
|
|
927
|
+
```yaml
|
|
928
|
+
filePath: /Users/morse/Documents/GitHub/kimakivoice/discord/src/interaction-handler.ts
|
|
929
|
+
offset: 1
|
|
930
|
+
limit: 260
|
|
931
|
+
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
**Output:**
|
|
935
|
+
```
|
|
936
|
+
<file>
|
|
937
|
+
00002| // Processes all slash commands (/session, /resume, /fork, /model, /abort, etc.)
|
|
938
|
+
00003| // and manages autocomplete, select menu interactions for the bot.
|
|
939
|
+
00004|
|
|
940
|
+
00005| import { Events, type Client, type Interaction } from 'discord.js'
|
|
941
|
+
00006| import { handleSessionCommand, handleSessionAutocomplete } from './commands/session.js'
|
|
942
|
+
00007| import { handleNewWorktreeCommand } from './commands/worktree.js'
|
|
943
|
+
00008| import { handleMergeWorktreeCommand } from './commands/merge-worktree.js'
|
|
944
|
+
00009| import {
|
|
945
|
+
00010| handleEnableWorktreesCommand,
|
|
946
|
+
00011| handleDisableWorktreesCommand,
|
|
947
|
+
00012| } from './commands/worktree-settings.js'
|
|
948
|
+
00013| import { handleResumeCommand, handleResumeAutocomplete } from './commands/resume.js'
|
|
949
|
+
00014| import { handleAddProjectCommand, handleAddProjectAutocomplete } from './commands/add-project.js'
|
|
950
|
+
00015| import {
|
|
951
|
+
00016| handleRemoveProjectCommand,
|
|
952
|
+
00017| handleRemoveProjectAutocomplete,
|
|
953
|
+
00018| } from './commands/remove-project.js'
|
|
954
|
+
00019| import { handleCreateNewProjectCommand } from './commands/create-new-project.js'
|
|
955
|
+
00020| import { handlePermissionSelectMenu } from './commands/permissions.js'
|
|
956
|
+
00021| import { handleAbortCommand } from './commands/abort.js'
|
|
957
|
+
00022| import { handleShareCommand } from './commands/share.js'
|
|
958
|
+
00023| import { handleForkCommand, handleForkSelectMenu } from './commands/fork.js'
|
|
959
|
+
00024| import {
|
|
960
|
+
00025| handleModelCommand,
|
|
961
|
+
00026| handleProviderSelectMenu,
|
|
962
|
+
00027| handleModelSelectMenu,
|
|
963
|
+
00028| } from './commands/model.js'
|
|
964
|
+
00029| import { handleAgentCommand, handleAgentSelectMenu, handleQuickAgentCommand } from './commands/agent.js'
|
|
965
|
+
00030| import { handleAskQuestionSelectMenu } from './commands/ask-question.js'
|
|
966
|
+
00031| import { handleQueueCommand, handleClearQueueCommand } from './commands/queue.js'
|
|
967
|
+
00032| import { handleUndoCommand, handleRedoCommand } from './commands/undo-redo.js'
|
|
968
|
+
00033| import { handleUserCommand } from './commands/user-command.js'
|
|
969
|
+
00034| import { createLogger } from './logger.js'
|
|
970
|
+
00035|
|
|
971
|
+
00036| const interactionLogger = createLogger('INTERACTION')
|
|
972
|
+
00037|
|
|
973
|
+
00038| export function registerInteractionHandler({
|
|
974
|
+
00039| discordClient,
|
|
975
|
+
00040| appId,
|
|
976
|
+
00041| }: {
|
|
977
|
+
00042| discordClient: Client
|
|
978
|
+
00043| appId: string
|
|
979
|
+
00044| }) {
|
|
980
|
+
00045| interactionLogger.log('[REGISTER] Interaction handler registered')
|
|
981
|
+
00046|
|
|
982
|
+
00047| discordClient.on(Events.InteractionCreate, async (interaction: Interaction) => {
|
|
983
|
+
00048| try {
|
|
984
|
+
00049| interactionLogger.log(
|
|
985
|
+
00050| `[INTERACTION] Received: ${interaction.type} - ${
|
|
986
|
+
00051| interaction.isChatInputCommand()
|
|
987
|
+
00052| ? interaction.commandName
|
|
988
|
+
00053| : interaction.isAutocomplete()
|
|
989
|
+
00054| ? `autocomplete:${interaction.commandName}`
|
|
990
|
+
00055| : 'other'
|
|
991
|
+
00056| }`,
|
|
992
|
+
00057| )
|
|
993
|
+
00058|
|
|
994
|
+
00059| if (interaction.isAutocomplete()) {
|
|
995
|
+
00060| switch (interaction.commandName) {
|
|
996
|
+
00061| case 'new-session':
|
|
997
|
+
00062| await handleSessionAutocomplete({ interaction, appId })
|
|
998
|
+
00063| return
|
|
999
|
+
00064|
|
|
1000
|
+
00065| case 'resume':
|
|
1001
|
+
00066| await handleResumeAutocomplete({ interaction, appId })
|
|
1002
|
+
00067| return
|
|
1003
|
+
00068|
|
|
1004
|
+
00069| case 'add-project':
|
|
1005
|
+
00070| await handleAddProjectAutocomplete({ interaction, appId })
|
|
1006
|
+
00071| return
|
|
1007
|
+
00072|
|
|
1008
|
+
00073| case 'remove-project':
|
|
1009
|
+
00074| await handleRemoveProjectAutocomplete({ interaction, appId })
|
|
1010
|
+
00075| return
|
|
1011
|
+
00076|
|
|
1012
|
+
00077| default:
|
|
1013
|
+
00078| await interaction.respond([])
|
|
1014
|
+
00079| return
|
|
1015
|
+
00080| }
|
|
1016
|
+
00081| }
|
|
1017
|
+
00082|
|
|
1018
|
+
00083| if (interaction.isChatInputCommand()) {
|
|
1019
|
+
00084| interactionLogger.log(`[COMMAND] Processing: ${interaction.commandName}`)
|
|
1020
|
+
00085|
|
|
1021
|
+
00086| switch (interaction.commandName) {
|
|
1022
|
+
00087| case 'new-session':
|
|
1023
|
+
00088| await handleSessionCommand({ command: interaction, appId })
|
|
1024
|
+
00089| return
|
|
1025
|
+
00090|
|
|
1026
|
+
00091| case 'new-worktree':
|
|
1027
|
+
00092| await handleNewWorktreeCommand({ command: interaction, appId })
|
|
1028
|
+
00093| return
|
|
1029
|
+
00094|
|
|
1030
|
+
00095| case 'merge-worktree':
|
|
1031
|
+
00096| await handleMergeWorktreeCommand({ command: interaction, appId })
|
|
1032
|
+
00097| return
|
|
1033
|
+
00098|
|
|
1034
|
+
00099| case 'enable-worktrees':
|
|
1035
|
+
00100| await handleEnableWorktreesCommand({ command: interaction, appId })
|
|
1036
|
+
00101| return
|
|
1037
|
+
00102|
|
|
1038
|
+
00103| case 'disable-worktrees':
|
|
1039
|
+
00104| await handleDisableWorktreesCommand({ command: interaction, appId })
|
|
1040
|
+
00105| return
|
|
1041
|
+
00106|
|
|
1042
|
+
00107| case 'resume':
|
|
1043
|
+
00108| await handleResumeCommand({ command: interaction, appId })
|
|
1044
|
+
00109| return
|
|
1045
|
+
00110|
|
|
1046
|
+
00111| case 'add-project':
|
|
1047
|
+
00112| await handleAddProjectCommand({ command: interaction, appId })
|
|
1048
|
+
00113| return
|
|
1049
|
+
00114|
|
|
1050
|
+
00115| case 'remove-project':
|
|
1051
|
+
00116| await handleRemoveProjectCommand({ command: interaction, appId })
|
|
1052
|
+
00117| return
|
|
1053
|
+
00118|
|
|
1054
|
+
00119| case 'create-new-project':
|
|
1055
|
+
00120| await handleCreateNewProjectCommand({ command: interaction, appId })
|
|
1056
|
+
00121| return
|
|
1057
|
+
00122|
|
|
1058
|
+
00123| case 'abort':
|
|
1059
|
+
00124| case 'stop':
|
|
1060
|
+
00125| await handleAbortCommand({ command: interaction, appId })
|
|
1061
|
+
00126| return
|
|
1062
|
+
00127|
|
|
1063
|
+
00128| case 'share':
|
|
1064
|
+
00129| await handleShareCommand({ command: interaction, appId })
|
|
1065
|
+
00130| return
|
|
1066
|
+
00131|
|
|
1067
|
+
00132| case 'fork':
|
|
1068
|
+
00133| await handleForkCommand(interaction)
|
|
1069
|
+
00134| return
|
|
1070
|
+
00135|
|
|
1071
|
+
00136| case 'model':
|
|
1072
|
+
00137| await handleModelCommand({ interaction, appId })
|
|
1073
|
+
00138| return
|
|
1074
|
+
00139|
|
|
1075
|
+
00140| case 'agent':
|
|
1076
|
+
00141| await handleAgentCommand({ interaction, appId })
|
|
1077
|
+
00142| return
|
|
1078
|
+
00143|
|
|
1079
|
+
00144| case 'queue':
|
|
1080
|
+
00145| await handleQueueCommand({ command: interaction, appId })
|
|
1081
|
+
00146| return
|
|
1082
|
+
00147|
|
|
1083
|
+
00148| case 'clear-queue':
|
|
1084
|
+
00149| await handleClearQueueCommand({ command: interaction, appId })
|
|
1085
|
+
00150| return
|
|
1086
|
+
00151|
|
|
1087
|
+
00152| case 'undo':
|
|
1088
|
+
00153| await handleUndoCommand({ command: interaction, appId })
|
|
1089
|
+
00154| return
|
|
1090
|
+
00155|
|
|
1091
|
+
00156| case 'redo':
|
|
1092
|
+
00157| await handleRedoCommand({ command: interaction, appId })
|
|
1093
|
+
00158| return
|
|
1094
|
+
00159| }
|
|
1095
|
+
00160|
|
|
1096
|
+
00161| // Handle quick agent commands (ending with -agent suffix, but not the base /agent command)
|
|
1097
|
+
00162| if (interaction.commandName.endsWith('-agent') && interaction.commandName !== 'agent') {
|
|
1098
|
+
00163| await handleQuickAgentCommand({ command: interaction, appId })
|
|
1099
|
+
00164| return
|
|
1100
|
+
00165| }
|
|
1101
|
+
00166|
|
|
1102
|
+
00167| // Handle user-defined commands (ending with -cmd suffix)
|
|
1103
|
+
00168| if (interaction.commandName.endsWith('-cmd')) {
|
|
1104
|
+
00169| await handleUserCommand({ command: interaction, appId })
|
|
1105
|
+
00170| return
|
|
1106
|
+
00171| }
|
|
1107
|
+
00172| return
|
|
1108
|
+
00173| }
|
|
1109
|
+
00174|
|
|
1110
|
+
00175| if (interaction.isStringSelectMenu()) {
|
|
1111
|
+
00176| const customId = interaction.customId
|
|
1112
|
+
00177|
|
|
1113
|
+
00178| if (customId.startsWith('fork_select:')) {
|
|
1114
|
+
00179| await handleForkSelectMenu(interaction)
|
|
1115
|
+
00180| return
|
|
1116
|
+
00181| }
|
|
1117
|
+
00182|
|
|
1118
|
+
00183| if (customId.startsWith('model_provider:')) {
|
|
1119
|
+
00184| await handleProviderSelectMenu(interaction)
|
|
1120
|
+
00185| return
|
|
1121
|
+
00186| }
|
|
1122
|
+
00187|
|
|
1123
|
+
00188| if (customId.startsWith('model_select:')) {
|
|
1124
|
+
00189| await handleModelSelectMenu(interaction)
|
|
1125
|
+
00190| return
|
|
1126
|
+
00191| }
|
|
1127
|
+
00192|
|
|
1128
|
+
00193| if (customId.startsWith('agent_select:')) {
|
|
1129
|
+
00194| await handleAgentSelectMenu(interaction)
|
|
1130
|
+
00195| return
|
|
1131
|
+
00196| }
|
|
1132
|
+
00197|
|
|
1133
|
+
00198| if (customId.startsWith('ask_question:')) {
|
|
1134
|
+
00199| await handleAskQuestionSelectMenu(interaction)
|
|
1135
|
+
00200| return
|
|
1136
|
+
00201| }
|
|
1137
|
+
00202|
|
|
1138
|
+
00203| if (customId.startsWith('permission:')) {
|
|
1139
|
+
00204| await handlePermissionSelectMenu(interaction)
|
|
1140
|
+
00205| return
|
|
1141
|
+
00206| }
|
|
1142
|
+
00207| return
|
|
1143
|
+
00208| }
|
|
1144
|
+
00209| } catch (error) {
|
|
1145
|
+
00210| interactionLogger.error('[INTERACTION] Error handling interaction:', error)
|
|
1146
|
+
00211| try {
|
|
1147
|
+
00212| if (interaction.isRepliable() && !interaction.replied && !interaction.deferred) {
|
|
1148
|
+
00213| await interaction.reply({
|
|
1149
|
+
00214| content: 'An error occurred processing this command.',
|
|
1150
|
+
00215| ephemeral: true,
|
|
1151
|
+
00216| })
|
|
1152
|
+
00217| }
|
|
1153
|
+
00218| } catch (replyError) {
|
|
1154
|
+
00219| interactionLogger.error('[INTERACTION] Failed to send error reply:', replyError)
|
|
1155
|
+
00220| }
|
|
1156
|
+
00221| }
|
|
1157
|
+
00222| })
|
|
1158
|
+
00223| }
|
|
1159
|
+
00224|
|
|
1160
|
+
|
|
1161
|
+
(End of file - total 224 lines)
|
|
1162
|
+
</file>
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
*Completed in 3.5s*
|
|
1167
|
+
|
|
1168
|
+
### π€ Assistant (gpt-5.2-codex)
|
|
1169
|
+
|
|
1170
|
+
**Started using openai/gpt-5.2-codex**
|
|
1171
|
+
|
|
1172
|
+
#### π οΈ Tool: read
|
|
1173
|
+
|
|
1174
|
+
**Input:**
|
|
1175
|
+
```yaml
|
|
1176
|
+
filePath: /Users/morse/Documents/GitHub/kimakivoice/discord/src/session-handler.ts
|
|
1177
|
+
offset: 1
|
|
1178
|
+
limit: 140
|
|
1179
|
+
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
**Output:**
|
|
1183
|
+
```
|
|
1184
|
+
<file>
|
|
1185
|
+
00002| // Creates, maintains, and sends prompts to OpenCode sessions from Discord threads.
|
|
1186
|
+
00003| // Handles streaming events, permissions, abort signals, and message queuing.
|
|
1187
|
+
00004|
|
|
1188
|
+
00005| import type { Part, PermissionRequest, QuestionRequest } from '@opencode-ai/sdk/v2'
|
|
1189
|
+
00006| import type { FilePartInput } from '@opencode-ai/sdk'
|
|
1190
|
+
00007| import type { Message, ThreadChannel } from 'discord.js'
|
|
1191
|
+
00008| import prettyMilliseconds from 'pretty-ms'
|
|
1192
|
+
00009| import {
|
|
1193
|
+
00010| getDatabase,
|
|
1194
|
+
00011| getSessionModel,
|
|
1195
|
+
00012| getChannelModel,
|
|
1196
|
+
00013| getSessionAgent,
|
|
1197
|
+
00014| getChannelAgent,
|
|
1198
|
+
00015| setSessionAgent,
|
|
1199
|
+
00016| getThreadWorktree,
|
|
1200
|
+
00017| } from './database.js'
|
|
1201
|
+
00018| import {
|
|
1202
|
+
00019| initializeOpencodeForDirectory,
|
|
1203
|
+
00020| getOpencodeServers,
|
|
1204
|
+
00021| getOpencodeClientV2,
|
|
1205
|
+
00022| } from './opencode.js'
|
|
1206
|
+
00023| import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from './discord-utils.js'
|
|
1207
|
+
00024| import { formatPart } from './message-formatting.js'
|
|
1208
|
+
00025| import { getOpencodeSystemMessage, type WorktreeInfo } from './system-message.js'
|
|
1209
|
+
00026| import { createLogger } from './logger.js'
|
|
1210
|
+
00027| import { isAbortError } from './utils.js'
|
|
1211
|
+
00028| import {
|
|
1212
|
+
00029| showAskUserQuestionDropdowns,
|
|
1213
|
+
00030| cancelPendingQuestion,
|
|
1214
|
+
00031| pendingQuestionContexts,
|
|
1215
|
+
00032| } from './commands/ask-question.js'
|
|
1216
|
+
00033| import { showPermissionDropdown, cleanupPermissionContext } from './commands/permissions.js'
|
|
1217
|
+
00034| import * as errore from 'errore'
|
|
1218
|
+
00035|
|
|
1219
|
+
00036| const sessionLogger = createLogger('SESSION')
|
|
1220
|
+
00037| const voiceLogger = createLogger('VOICE')
|
|
1221
|
+
00038| const discordLogger = createLogger('DISCORD')
|
|
1222
|
+
00039|
|
|
1223
|
+
00040| export const abortControllers = new Map<string, AbortController>()
|
|
1224
|
+
00041|
|
|
1225
|
+
00042| // Track multiple pending permissions per thread (keyed by permission ID)
|
|
1226
|
+
00043| // OpenCode handles blocking/sequencing - we just need to track all pending permissions
|
|
1227
|
+
00044| // to avoid duplicates and properly clean up on auto-reject
|
|
1228
|
+
00045| export const pendingPermissions = new Map<
|
|
1229
|
+
00046| string, // threadId
|
|
1230
|
+
00047| Map<string, { permission: PermissionRequest; messageId: string; directory: string; contextHash: string }> // permissionId -> data
|
|
1231
|
+
00048| >()
|
|
1232
|
+
00049|
|
|
1233
|
+
00050| export type QueuedMessage = {
|
|
1234
|
+
00051| prompt: string
|
|
1235
|
+
00052| userId: string
|
|
1236
|
+
00053| username: string
|
|
1237
|
+
00054| queuedAt: number
|
|
1238
|
+
00055| images?: FilePartInput[]
|
|
1239
|
+
00056| }
|
|
1240
|
+
00057|
|
|
1241
|
+
00058| // Queue of messages waiting to be sent after current response finishes
|
|
1242
|
+
00059| // Key is threadId, value is array of queued messages
|
|
1243
|
+
00060| export const messageQueue = new Map<string, QueuedMessage[]>()
|
|
1244
|
+
00061|
|
|
1245
|
+
00062| export function addToQueue({
|
|
1246
|
+
00063| threadId,
|
|
1247
|
+
00064| message,
|
|
1248
|
+
00065| }: {
|
|
1249
|
+
00066| threadId: string
|
|
1250
|
+
00067| message: QueuedMessage
|
|
1251
|
+
00068| }): number {
|
|
1252
|
+
00069| const queue = messageQueue.get(threadId) || []
|
|
1253
|
+
00070| queue.push(message)
|
|
1254
|
+
00071| messageQueue.set(threadId, queue)
|
|
1255
|
+
00072| return queue.length
|
|
1256
|
+
00073| }
|
|
1257
|
+
00074|
|
|
1258
|
+
00075| export function getQueueLength(threadId: string): number {
|
|
1259
|
+
00076| return messageQueue.get(threadId)?.length || 0
|
|
1260
|
+
00077| }
|
|
1261
|
+
00078|
|
|
1262
|
+
00079| export function clearQueue(threadId: string): void {
|
|
1263
|
+
00080| messageQueue.delete(threadId)
|
|
1264
|
+
00081| }
|
|
1265
|
+
00082|
|
|
1266
|
+
00083| /**
|
|
1267
|
+
00084| * Abort a running session and retry with the last user message.
|
|
1268
|
+
00085| * Used when model preference changes mid-request.
|
|
1269
|
+
00086| * Fetches last user message from OpenCode API instead of tracking in memory.
|
|
1270
|
+
00087| * @returns true if aborted and retry scheduled, false if no active request
|
|
1271
|
+
00088| */
|
|
1272
|
+
00089| export async function abortAndRetrySession({
|
|
1273
|
+
00090| sessionId,
|
|
1274
|
+
00091| thread,
|
|
1275
|
+
00092| projectDirectory,
|
|
1276
|
+
00093| }: {
|
|
1277
|
+
00094| sessionId: string
|
|
1278
|
+
00095| thread: ThreadChannel
|
|
1279
|
+
00096| projectDirectory: string
|
|
1280
|
+
00097| }): Promise<boolean> {
|
|
1281
|
+
00098| const controller = abortControllers.get(sessionId)
|
|
1282
|
+
00099|
|
|
1283
|
+
00100| if (!controller) {
|
|
1284
|
+
00101| sessionLogger.log(`[ABORT+RETRY] No active request for session ${sessionId}`)
|
|
1285
|
+
00102| return false
|
|
1286
|
+
00103| }
|
|
1287
|
+
00104|
|
|
1288
|
+
00105| sessionLogger.log(`[ABORT+RETRY] Aborting session ${sessionId} for model change`)
|
|
1289
|
+
00106|
|
|
1290
|
+
00107| // Abort with special reason so we don't show "completed" message
|
|
1291
|
+
00108| controller.abort('model-change')
|
|
1292
|
+
00109|
|
|
1293
|
+
00110| // Also call the API abort endpoint
|
|
1294
|
+
00111| const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
1295
|
+
00112| if (getClient instanceof Error) {
|
|
1296
|
+
00113| sessionLogger.error(`[ABORT+RETRY] Failed to initialize OpenCode client:`, getClient.message)
|
|
1297
|
+
00114| return false
|
|
1298
|
+
00115| }
|
|
1299
|
+
00116| const abortResult = await errore.tryAsync(() => {
|
|
1300
|
+
00117| return getClient().session.abort({ path: { id: sessionId } })
|
|
1301
|
+
00118| })
|
|
1302
|
+
00119| if (abortResult instanceof Error) {
|
|
1303
|
+
00120| sessionLogger.log(`[ABORT+RETRY] API abort call failed (may already be done):`, abortResult)
|
|
1304
|
+
00121| }
|
|
1305
|
+
00122|
|
|
1306
|
+
00123| // Small delay to let the abort propagate
|
|
1307
|
+
00124| await new Promise((resolve) => {
|
|
1308
|
+
00125| setTimeout(resolve, 300)
|
|
1309
|
+
00126| })
|
|
1310
|
+
00127|
|
|
1311
|
+
00128| // Fetch last user message from API
|
|
1312
|
+
00129| sessionLogger.log(`[ABORT+RETRY] Fetching last user message for session ${sessionId}`)
|
|
1313
|
+
00130| const messagesResponse = await getClient().session.messages({ path: { id: sessionId } })
|
|
1314
|
+
00131| const messages = messagesResponse.data || []
|
|
1315
|
+
00132| const lastUserMessage = [...messages].reverse().find((m) => m.info.role === 'user')
|
|
1316
|
+
00133|
|
|
1317
|
+
00134| if (!lastUserMessage) {
|
|
1318
|
+
00135| sessionLogger.log(`[ABORT+RETRY] No user message found in session ${sessionId}`)
|
|
1319
|
+
00136| return false
|
|
1320
|
+
00137| }
|
|
1321
|
+
00138|
|
|
1322
|
+
00139| // Extract text and images from parts
|
|
1323
|
+
00140| const textPart = lastUserMessage.parts.find((p) => p.type === 'text') as
|
|
1324
|
+
00141| | { type: 'text'; text: string }
|
|
1325
|
+
|
|
1326
|
+
(File has more lines. Use 'offset' parameter to read beyond line 141)
|
|
1327
|
+
</file>
|
|
1328
|
+
```
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
*Completed in 4.7s*
|
|
1332
|
+
|
|
1333
|
+
### π€ Assistant (gpt-5.2-codex)
|
|
1334
|
+
|
|
1335
|
+
**Started using openai/gpt-5.2-codex**
|
|
1336
|
+
|
|
1337
|
+
Got it. I found the permission dropdown logic in `discord/src/session-handler.ts` and `discord/src/commands/permissions.ts`. The duplication is currently only guarded by `permission.id`, so multiple requests with the same patterns can still render multiple dropdowns.
|
|
1338
|
+
|
|
1339
|
+
Plan:
|
|
1340
|
+
- Inspect pending permission tracking in `discord/src/session-handler.ts` and confirm how `pendingPermissions` is keyed and cleaned up with `cleanupPermissionContext` from `discord/src/commands/permissions.ts`.
|
|
1341
|
+
- Introduce a stable βdedupe keyβ (likely `permission.permission` + normalized `permission.patterns` + directory) and check against existing pending entries before calling `showPermissionDropdown`.
|
|
1342
|
+
- Store this dedupe key alongside the pending permission entry so when a permission is replied or auto-rejected, the key can be removed/updated correctly.
|
|
1343
|
+
- Add logging to make it clear when a permission request is ignored because it matches an existing pattern.
|
|
1344
|
+
|
|
1345
|
+
If you want a specific dedupe rule (e.g., case sensitivity, order of patterns, per-session vs per-thread), tell me and Iβll adjust the plan.
|
|
1346
|
+
|
|
1347
|
+
Now what?
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
*Completed in 10.8s*
|