kimaki 0.4.34 → 0.4.35
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 +7 -0
- package/dist/commands/session.js +55 -0
- package/dist/opencode.js +1 -2
- package/dist/session-handler.js +18 -11
- package/dist/unnest-code-blocks.js +4 -2
- package/dist/unnest-code-blocks.test.js +40 -15
- package/package.json +1 -1
- package/src/cli.ts +8 -0
- package/src/commands/session.ts +68 -0
- package/src/opencode.ts +1 -2
- package/src/session-handler.ts +21 -15
- package/src/unnest-code-blocks.test.ts +42 -15
- package/src/unnest-code-blocks.ts +4 -2
package/dist/cli.js
CHANGED
|
@@ -135,6 +135,13 @@ async function registerCommands(token, appId, userCommands = []) {
|
|
|
135
135
|
.setAutocomplete(true)
|
|
136
136
|
.setMaxLength(6000);
|
|
137
137
|
return option;
|
|
138
|
+
})
|
|
139
|
+
.addStringOption((option) => {
|
|
140
|
+
option
|
|
141
|
+
.setName('agent')
|
|
142
|
+
.setDescription('Agent to use for this session')
|
|
143
|
+
.setAutocomplete(true);
|
|
144
|
+
return option;
|
|
138
145
|
})
|
|
139
146
|
.toJSON(),
|
|
140
147
|
new SlashCommandBuilder()
|
package/dist/commands/session.js
CHANGED
|
@@ -13,6 +13,7 @@ export async function handleSessionCommand({ command, appId, }) {
|
|
|
13
13
|
await command.deferReply({ ephemeral: false });
|
|
14
14
|
const prompt = command.options.getString('prompt', true);
|
|
15
15
|
const filesString = command.options.getString('files') || '';
|
|
16
|
+
const agent = command.options.getString('agent') || undefined;
|
|
16
17
|
const channel = command.channel;
|
|
17
18
|
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
18
19
|
await command.editReply('This command can only be used in text channels');
|
|
@@ -66,6 +67,7 @@ export async function handleSessionCommand({ command, appId, }) {
|
|
|
66
67
|
thread,
|
|
67
68
|
projectDirectory,
|
|
68
69
|
channelId: textChannel.id,
|
|
70
|
+
agent,
|
|
69
71
|
});
|
|
70
72
|
}
|
|
71
73
|
catch (error) {
|
|
@@ -73,8 +75,61 @@ export async function handleSessionCommand({ command, appId, }) {
|
|
|
73
75
|
await command.editReply(`Failed to create session: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
74
76
|
}
|
|
75
77
|
}
|
|
78
|
+
async function handleAgentAutocomplete({ interaction, appId, }) {
|
|
79
|
+
const focusedValue = interaction.options.getFocused();
|
|
80
|
+
let projectDirectory;
|
|
81
|
+
if (interaction.channel) {
|
|
82
|
+
const channel = interaction.channel;
|
|
83
|
+
if (channel.type === ChannelType.GuildText) {
|
|
84
|
+
const textChannel = channel;
|
|
85
|
+
if (textChannel.topic) {
|
|
86
|
+
const extracted = extractTagsArrays({
|
|
87
|
+
xml: textChannel.topic,
|
|
88
|
+
tags: ['kimaki.directory', 'kimaki.app'],
|
|
89
|
+
});
|
|
90
|
+
const channelAppId = extracted['kimaki.app']?.[0]?.trim();
|
|
91
|
+
if (channelAppId && channelAppId !== appId) {
|
|
92
|
+
await interaction.respond([]);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!projectDirectory) {
|
|
100
|
+
await interaction.respond([]);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
105
|
+
const agentsResponse = await getClient().app.agents({
|
|
106
|
+
query: { directory: projectDirectory },
|
|
107
|
+
});
|
|
108
|
+
if (!agentsResponse.data || agentsResponse.data.length === 0) {
|
|
109
|
+
await interaction.respond([]);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const agents = agentsResponse.data
|
|
113
|
+
.filter((a) => a.mode === 'primary' || a.mode === 'all')
|
|
114
|
+
.filter((a) => a.name.toLowerCase().includes(focusedValue.toLowerCase()))
|
|
115
|
+
.slice(0, 25);
|
|
116
|
+
const choices = agents.map((agent) => ({
|
|
117
|
+
name: agent.name.slice(0, 100),
|
|
118
|
+
value: agent.name,
|
|
119
|
+
}));
|
|
120
|
+
await interaction.respond(choices);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
logger.error('[AUTOCOMPLETE] Error fetching agents:', error);
|
|
124
|
+
await interaction.respond([]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
76
127
|
export async function handleSessionAutocomplete({ interaction, appId, }) {
|
|
77
128
|
const focusedOption = interaction.options.getFocused(true);
|
|
129
|
+
if (focusedOption.name === 'agent') {
|
|
130
|
+
await handleAgentAutocomplete({ interaction, appId });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
78
133
|
if (focusedOption.name !== 'files') {
|
|
79
134
|
return;
|
|
80
135
|
}
|
package/dist/opencode.js
CHANGED
|
@@ -87,8 +87,7 @@ export async function initializeOpencodeForDirectory(directory) {
|
|
|
87
87
|
throw new Error(`Directory does not exist or is not accessible: ${directory}`);
|
|
88
88
|
}
|
|
89
89
|
const port = await getOpenPort();
|
|
90
|
-
const
|
|
91
|
-
const opencodeCommand = process.env.OPENCODE_PATH || `${opencodeBinDir}/opencode`;
|
|
90
|
+
const opencodeCommand = process.env.OPENCODE_PATH || 'opencode';
|
|
92
91
|
const serverProcess = spawn(opencodeCommand, ['serve', '--port', port.toString()], {
|
|
93
92
|
stdio: 'pipe',
|
|
94
93
|
detached: false,
|
package/dist/session-handler.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Creates, maintains, and sends prompts to OpenCode sessions from Discord threads.
|
|
3
3
|
// Handles streaming events, permissions, abort signals, and message queuing.
|
|
4
4
|
import prettyMilliseconds from 'pretty-ms';
|
|
5
|
-
import { getDatabase, getSessionModel, getChannelModel, getSessionAgent, getChannelAgent } from './database.js';
|
|
5
|
+
import { getDatabase, getSessionModel, getChannelModel, getSessionAgent, getChannelAgent, setSessionAgent } from './database.js';
|
|
6
6
|
import { initializeOpencodeForDirectory, getOpencodeServers, getOpencodeClientV2 } from './opencode.js';
|
|
7
7
|
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from './discord-utils.js';
|
|
8
8
|
import { formatPart } from './message-formatting.js';
|
|
@@ -85,7 +85,7 @@ export async function abortAndRetrySession({ sessionId, thread, projectDirectory
|
|
|
85
85
|
});
|
|
86
86
|
return true;
|
|
87
87
|
}
|
|
88
|
-
export async function handleOpencodeSession({ prompt, thread, projectDirectory, originalMessage, images = [], channelId, command, }) {
|
|
88
|
+
export async function handleOpencodeSession({ prompt, thread, projectDirectory, originalMessage, images = [], channelId, command, agent, }) {
|
|
89
89
|
voiceLogger.log(`[OPENCODE SESSION] Starting for thread ${thread.id} with prompt: "${prompt.slice(0, 50)}${prompt.length > 50 ? '...' : ''}"`);
|
|
90
90
|
const sessionStartTime = Date.now();
|
|
91
91
|
const directory = projectDirectory || process.cwd();
|
|
@@ -127,6 +127,11 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
127
127
|
.prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
|
|
128
128
|
.run(thread.id, session.id);
|
|
129
129
|
sessionLogger.log(`Stored session ${session.id} for thread ${thread.id}`);
|
|
130
|
+
// Store agent preference if provided
|
|
131
|
+
if (agent) {
|
|
132
|
+
setSessionAgent(session.id, agent);
|
|
133
|
+
sessionLogger.log(`Set agent preference for session ${session.id}: ${agent}`);
|
|
134
|
+
}
|
|
130
135
|
const existingController = abortControllers.get(session.id);
|
|
131
136
|
if (existingController) {
|
|
132
137
|
voiceLogger.log(`[ABORT] Cancelling existing request for session: ${session.id}`);
|
|
@@ -648,15 +653,17 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
648
653
|
discordLogger.log(`Could not update reaction:`, e);
|
|
649
654
|
}
|
|
650
655
|
}
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
656
|
+
const errorDisplay = (() => {
|
|
657
|
+
if (error instanceof Error) {
|
|
658
|
+
const name = error.constructor.name || 'Error';
|
|
659
|
+
return `[${name}]\n${error.stack || error.message}`;
|
|
660
|
+
}
|
|
661
|
+
if (typeof error === 'string') {
|
|
662
|
+
return error;
|
|
663
|
+
}
|
|
664
|
+
return String(error);
|
|
665
|
+
})();
|
|
666
|
+
await sendThreadMessage(thread, `✗ Unexpected bot Error: ${errorDisplay}`);
|
|
660
667
|
}
|
|
661
668
|
}
|
|
662
669
|
}
|
|
@@ -107,9 +107,11 @@ function renderSegments(segments) {
|
|
|
107
107
|
}
|
|
108
108
|
else {
|
|
109
109
|
// Raw content (no prefix means it's original raw)
|
|
110
|
-
|
|
110
|
+
// Ensure raw ends with newline for proper separation from next segment
|
|
111
|
+
const raw = segment.content.trimEnd();
|
|
112
|
+
result.push(raw + '\n');
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
|
-
return result.join('');
|
|
116
|
+
return result.join('').trimEnd();
|
|
115
117
|
}
|
|
@@ -11,8 +11,7 @@ test('basic - single item with code block', () => {
|
|
|
11
11
|
|
|
12
12
|
\`\`\`js
|
|
13
13
|
const x = 1
|
|
14
|
-
\`\`\`
|
|
15
|
-
"
|
|
14
|
+
\`\`\`"
|
|
16
15
|
`);
|
|
17
16
|
});
|
|
18
17
|
test('multiple items - code in middle item only', () => {
|
|
@@ -50,8 +49,7 @@ test('multiple code blocks in one item', () => {
|
|
|
50
49
|
\`\`\`
|
|
51
50
|
\`\`\`python
|
|
52
51
|
b = 2
|
|
53
|
-
\`\`\`
|
|
54
|
-
"
|
|
52
|
+
\`\`\`"
|
|
55
53
|
`);
|
|
56
54
|
});
|
|
57
55
|
test('nested list with code', () => {
|
|
@@ -125,8 +123,7 @@ test('mixed - some items have code, some dont', () => {
|
|
|
125
123
|
|
|
126
124
|
\`\`\`python
|
|
127
125
|
y = 2
|
|
128
|
-
\`\`\`
|
|
129
|
-
"
|
|
126
|
+
\`\`\`"
|
|
130
127
|
`);
|
|
131
128
|
});
|
|
132
129
|
test('text before and after code in same item', () => {
|
|
@@ -142,8 +139,7 @@ test('text before and after code in same item', () => {
|
|
|
142
139
|
\`\`\`js
|
|
143
140
|
const x = 1
|
|
144
141
|
\`\`\`
|
|
145
|
-
- End text
|
|
146
|
-
"
|
|
142
|
+
- End text"
|
|
147
143
|
`);
|
|
148
144
|
});
|
|
149
145
|
test('preserves content outside lists', () => {
|
|
@@ -169,7 +165,6 @@ More text after.`;
|
|
|
169
165
|
const x = 1
|
|
170
166
|
\`\`\`
|
|
171
167
|
|
|
172
|
-
|
|
173
168
|
More text after."
|
|
174
169
|
`);
|
|
175
170
|
});
|
|
@@ -195,8 +190,7 @@ test('handles code block without language', () => {
|
|
|
195
190
|
|
|
196
191
|
\`\`\`
|
|
197
192
|
plain code
|
|
198
|
-
\`\`\`
|
|
199
|
-
"
|
|
193
|
+
\`\`\`"
|
|
200
194
|
`);
|
|
201
195
|
});
|
|
202
196
|
test('handles empty list item with code', () => {
|
|
@@ -207,8 +201,7 @@ test('handles empty list item with code', () => {
|
|
|
207
201
|
expect(result).toMatchInlineSnapshot(`
|
|
208
202
|
"\`\`\`js
|
|
209
203
|
const x = 1
|
|
210
|
-
\`\`\`
|
|
211
|
-
"
|
|
204
|
+
\`\`\`"
|
|
212
205
|
`);
|
|
213
206
|
});
|
|
214
207
|
test('numbered list with text after code block', () => {
|
|
@@ -321,7 +314,8 @@ test('deeply nested list with code', () => {
|
|
|
321
314
|
deep code
|
|
322
315
|
\`\`\`
|
|
323
316
|
- Text after deep code
|
|
324
|
-
- Another level 3
|
|
317
|
+
- Another level 3
|
|
318
|
+
- Back to level 2"
|
|
325
319
|
`);
|
|
326
320
|
});
|
|
327
321
|
test('nested numbered list inside unordered with code', () => {
|
|
@@ -342,7 +336,8 @@ test('nested numbered list inside unordered with code', () => {
|
|
|
342
336
|
code
|
|
343
337
|
\`\`\`
|
|
344
338
|
- Text after
|
|
345
|
-
2. Second nested
|
|
339
|
+
2. Second nested
|
|
340
|
+
- Another unordered"
|
|
346
341
|
`);
|
|
347
342
|
});
|
|
348
343
|
test('code block at end of numbered item no text after', () => {
|
|
@@ -405,3 +400,33 @@ test('code block immediately after list marker', () => {
|
|
|
405
400
|
2. Normal item"
|
|
406
401
|
`);
|
|
407
402
|
});
|
|
403
|
+
test('code block with filename metadata', () => {
|
|
404
|
+
const input = `- Item with code
|
|
405
|
+
\`\`\`tsx filename=example.tsx
|
|
406
|
+
const x = 1
|
|
407
|
+
\`\`\``;
|
|
408
|
+
const result = unnestCodeBlocksFromLists(input);
|
|
409
|
+
expect(result).toMatchInlineSnapshot(`
|
|
410
|
+
"- Item with code
|
|
411
|
+
|
|
412
|
+
\`\`\`tsx filename=example.tsx
|
|
413
|
+
const x = 1
|
|
414
|
+
\`\`\`"
|
|
415
|
+
`);
|
|
416
|
+
});
|
|
417
|
+
test('numbered list with filename metadata code block', () => {
|
|
418
|
+
const input = `1. First item
|
|
419
|
+
\`\`\`tsx filename=app.tsx
|
|
420
|
+
export default function App() {}
|
|
421
|
+
\`\`\`
|
|
422
|
+
2. Second item`;
|
|
423
|
+
const result = unnestCodeBlocksFromLists(input);
|
|
424
|
+
expect(result).toMatchInlineSnapshot(`
|
|
425
|
+
"1. First item
|
|
426
|
+
|
|
427
|
+
\`\`\`tsx filename=app.tsx
|
|
428
|
+
export default function App() {}
|
|
429
|
+
\`\`\`
|
|
430
|
+
2. Second item"
|
|
431
|
+
`);
|
|
432
|
+
});
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -197,6 +197,14 @@ async function registerCommands(token: string, appId: string, userCommands: Open
|
|
|
197
197
|
|
|
198
198
|
return option
|
|
199
199
|
})
|
|
200
|
+
.addStringOption((option) => {
|
|
201
|
+
option
|
|
202
|
+
.setName('agent')
|
|
203
|
+
.setDescription('Agent to use for this session')
|
|
204
|
+
.setAutocomplete(true)
|
|
205
|
+
|
|
206
|
+
return option
|
|
207
|
+
})
|
|
200
208
|
.toJSON(),
|
|
201
209
|
new SlashCommandBuilder()
|
|
202
210
|
.setName('add-project')
|
package/src/commands/session.ts
CHANGED
|
@@ -21,6 +21,7 @@ export async function handleSessionCommand({
|
|
|
21
21
|
|
|
22
22
|
const prompt = command.options.getString('prompt', true)
|
|
23
23
|
const filesString = command.options.getString('files') || ''
|
|
24
|
+
const agent = command.options.getString('agent') || undefined
|
|
24
25
|
const channel = command.channel
|
|
25
26
|
|
|
26
27
|
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
@@ -91,6 +92,7 @@ export async function handleSessionCommand({
|
|
|
91
92
|
thread,
|
|
92
93
|
projectDirectory,
|
|
93
94
|
channelId: textChannel.id,
|
|
95
|
+
agent,
|
|
94
96
|
})
|
|
95
97
|
} catch (error) {
|
|
96
98
|
logger.error('[SESSION] Error:', error)
|
|
@@ -100,12 +102,78 @@ export async function handleSessionCommand({
|
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
async function handleAgentAutocomplete({
|
|
106
|
+
interaction,
|
|
107
|
+
appId,
|
|
108
|
+
}: AutocompleteContext): Promise<void> {
|
|
109
|
+
const focusedValue = interaction.options.getFocused()
|
|
110
|
+
|
|
111
|
+
let projectDirectory: string | undefined
|
|
112
|
+
|
|
113
|
+
if (interaction.channel) {
|
|
114
|
+
const channel = interaction.channel
|
|
115
|
+
if (channel.type === ChannelType.GuildText) {
|
|
116
|
+
const textChannel = channel as TextChannel
|
|
117
|
+
if (textChannel.topic) {
|
|
118
|
+
const extracted = extractTagsArrays({
|
|
119
|
+
xml: textChannel.topic,
|
|
120
|
+
tags: ['kimaki.directory', 'kimaki.app'],
|
|
121
|
+
})
|
|
122
|
+
const channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
123
|
+
if (channelAppId && channelAppId !== appId) {
|
|
124
|
+
await interaction.respond([])
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!projectDirectory) {
|
|
133
|
+
await interaction.respond([])
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
139
|
+
|
|
140
|
+
const agentsResponse = await getClient().app.agents({
|
|
141
|
+
query: { directory: projectDirectory },
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
if (!agentsResponse.data || agentsResponse.data.length === 0) {
|
|
145
|
+
await interaction.respond([])
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const agents = agentsResponse.data
|
|
150
|
+
.filter((a) => a.mode === 'primary' || a.mode === 'all')
|
|
151
|
+
.filter((a) => a.name.toLowerCase().includes(focusedValue.toLowerCase()))
|
|
152
|
+
.slice(0, 25)
|
|
153
|
+
|
|
154
|
+
const choices = agents.map((agent) => ({
|
|
155
|
+
name: agent.name.slice(0, 100),
|
|
156
|
+
value: agent.name,
|
|
157
|
+
}))
|
|
158
|
+
|
|
159
|
+
await interaction.respond(choices)
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logger.error('[AUTOCOMPLETE] Error fetching agents:', error)
|
|
162
|
+
await interaction.respond([])
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
103
166
|
export async function handleSessionAutocomplete({
|
|
104
167
|
interaction,
|
|
105
168
|
appId,
|
|
106
169
|
}: AutocompleteContext): Promise<void> {
|
|
107
170
|
const focusedOption = interaction.options.getFocused(true)
|
|
108
171
|
|
|
172
|
+
if (focusedOption.name === 'agent') {
|
|
173
|
+
await handleAgentAutocomplete({ interaction, appId })
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
109
177
|
if (focusedOption.name !== 'files') {
|
|
110
178
|
return
|
|
111
179
|
}
|
package/src/opencode.ts
CHANGED
|
@@ -115,8 +115,7 @@ export async function initializeOpencodeForDirectory(directory: string) {
|
|
|
115
115
|
|
|
116
116
|
const port = await getOpenPort()
|
|
117
117
|
|
|
118
|
-
const
|
|
119
|
-
const opencodeCommand = process.env.OPENCODE_PATH || `${opencodeBinDir}/opencode`
|
|
118
|
+
const opencodeCommand = process.env.OPENCODE_PATH || 'opencode'
|
|
120
119
|
|
|
121
120
|
const serverProcess = spawn(
|
|
122
121
|
opencodeCommand,
|
package/src/session-handler.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Part, PermissionRequest } from '@opencode-ai/sdk/v2'
|
|
|
6
6
|
import type { FilePartInput } from '@opencode-ai/sdk'
|
|
7
7
|
import type { Message, ThreadChannel } from 'discord.js'
|
|
8
8
|
import prettyMilliseconds from 'pretty-ms'
|
|
9
|
-
import { getDatabase, getSessionModel, getChannelModel, getSessionAgent, getChannelAgent } from './database.js'
|
|
9
|
+
import { getDatabase, getSessionModel, getChannelModel, getSessionAgent, getChannelAgent, setSessionAgent } from './database.js'
|
|
10
10
|
import { initializeOpencodeForDirectory, getOpencodeServers, getOpencodeClientV2 } from './opencode.js'
|
|
11
11
|
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from './discord-utils.js'
|
|
12
12
|
import { formatPart } from './message-formatting.js'
|
|
@@ -141,6 +141,7 @@ export async function handleOpencodeSession({
|
|
|
141
141
|
images = [],
|
|
142
142
|
channelId,
|
|
143
143
|
command,
|
|
144
|
+
agent,
|
|
144
145
|
}: {
|
|
145
146
|
prompt: string
|
|
146
147
|
thread: ThreadChannel
|
|
@@ -150,6 +151,8 @@ export async function handleOpencodeSession({
|
|
|
150
151
|
channelId?: string
|
|
151
152
|
/** If set, uses session.command API instead of session.prompt */
|
|
152
153
|
command?: { name: string; arguments: string }
|
|
154
|
+
/** Agent to use for this session */
|
|
155
|
+
agent?: string
|
|
153
156
|
}): Promise<{ sessionID: string; result: any; port?: number } | undefined> {
|
|
154
157
|
voiceLogger.log(
|
|
155
158
|
`[OPENCODE SESSION] Starting for thread ${thread.id} with prompt: "${prompt.slice(0, 50)}${prompt.length > 50 ? '...' : ''}"`,
|
|
@@ -209,6 +212,12 @@ export async function handleOpencodeSession({
|
|
|
209
212
|
.run(thread.id, session.id)
|
|
210
213
|
sessionLogger.log(`Stored session ${session.id} for thread ${thread.id}`)
|
|
211
214
|
|
|
215
|
+
// Store agent preference if provided
|
|
216
|
+
if (agent) {
|
|
217
|
+
setSessionAgent(session.id, agent)
|
|
218
|
+
sessionLogger.log(`Set agent preference for session ${session.id}: ${agent}`)
|
|
219
|
+
}
|
|
220
|
+
|
|
212
221
|
const existingController = abortControllers.get(session.id)
|
|
213
222
|
if (existingController) {
|
|
214
223
|
voiceLogger.log(
|
|
@@ -825,20 +834,17 @@ export async function handleOpencodeSession({
|
|
|
825
834
|
discordLogger.log(`Could not update reaction:`, e)
|
|
826
835
|
}
|
|
827
836
|
}
|
|
828
|
-
const
|
|
829
|
-
error
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
typeof error
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
await sendThreadMessage(
|
|
839
|
-
thread,
|
|
840
|
-
`✗ Unexpected bot Error: [${errorName}]\n${errorMsg}`,
|
|
841
|
-
)
|
|
837
|
+
const errorDisplay = (() => {
|
|
838
|
+
if (error instanceof Error) {
|
|
839
|
+
const name = error.constructor.name || 'Error'
|
|
840
|
+
return `[${name}]\n${error.stack || error.message}`
|
|
841
|
+
}
|
|
842
|
+
if (typeof error === 'string') {
|
|
843
|
+
return error
|
|
844
|
+
}
|
|
845
|
+
return String(error)
|
|
846
|
+
})()
|
|
847
|
+
await sendThreadMessage(thread, `✗ Unexpected bot Error: ${errorDisplay}`)
|
|
842
848
|
}
|
|
843
849
|
}
|
|
844
850
|
}
|
|
@@ -12,8 +12,7 @@ test('basic - single item with code block', () => {
|
|
|
12
12
|
|
|
13
13
|
\`\`\`js
|
|
14
14
|
const x = 1
|
|
15
|
-
\`\`\`
|
|
16
|
-
"
|
|
15
|
+
\`\`\`"
|
|
17
16
|
`)
|
|
18
17
|
})
|
|
19
18
|
|
|
@@ -53,8 +52,7 @@ test('multiple code blocks in one item', () => {
|
|
|
53
52
|
\`\`\`
|
|
54
53
|
\`\`\`python
|
|
55
54
|
b = 2
|
|
56
|
-
\`\`\`
|
|
57
|
-
"
|
|
55
|
+
\`\`\`"
|
|
58
56
|
`)
|
|
59
57
|
})
|
|
60
58
|
|
|
@@ -132,8 +130,7 @@ test('mixed - some items have code, some dont', () => {
|
|
|
132
130
|
|
|
133
131
|
\`\`\`python
|
|
134
132
|
y = 2
|
|
135
|
-
\`\`\`
|
|
136
|
-
"
|
|
133
|
+
\`\`\`"
|
|
137
134
|
`)
|
|
138
135
|
})
|
|
139
136
|
|
|
@@ -150,8 +147,7 @@ test('text before and after code in same item', () => {
|
|
|
150
147
|
\`\`\`js
|
|
151
148
|
const x = 1
|
|
152
149
|
\`\`\`
|
|
153
|
-
- End text
|
|
154
|
-
"
|
|
150
|
+
- End text"
|
|
155
151
|
`)
|
|
156
152
|
})
|
|
157
153
|
|
|
@@ -178,7 +174,6 @@ More text after.`
|
|
|
178
174
|
const x = 1
|
|
179
175
|
\`\`\`
|
|
180
176
|
|
|
181
|
-
|
|
182
177
|
More text after."
|
|
183
178
|
`)
|
|
184
179
|
})
|
|
@@ -206,8 +201,7 @@ test('handles code block without language', () => {
|
|
|
206
201
|
|
|
207
202
|
\`\`\`
|
|
208
203
|
plain code
|
|
209
|
-
\`\`\`
|
|
210
|
-
"
|
|
204
|
+
\`\`\`"
|
|
211
205
|
`)
|
|
212
206
|
})
|
|
213
207
|
|
|
@@ -219,8 +213,7 @@ test('handles empty list item with code', () => {
|
|
|
219
213
|
expect(result).toMatchInlineSnapshot(`
|
|
220
214
|
"\`\`\`js
|
|
221
215
|
const x = 1
|
|
222
|
-
\`\`\`
|
|
223
|
-
"
|
|
216
|
+
\`\`\`"
|
|
224
217
|
`)
|
|
225
218
|
})
|
|
226
219
|
|
|
@@ -338,7 +331,8 @@ test('deeply nested list with code', () => {
|
|
|
338
331
|
deep code
|
|
339
332
|
\`\`\`
|
|
340
333
|
- Text after deep code
|
|
341
|
-
- Another level 3
|
|
334
|
+
- Another level 3
|
|
335
|
+
- Back to level 2"
|
|
342
336
|
`)
|
|
343
337
|
})
|
|
344
338
|
|
|
@@ -360,7 +354,8 @@ test('nested numbered list inside unordered with code', () => {
|
|
|
360
354
|
code
|
|
361
355
|
\`\`\`
|
|
362
356
|
- Text after
|
|
363
|
-
2. Second nested
|
|
357
|
+
2. Second nested
|
|
358
|
+
- Another unordered"
|
|
364
359
|
`)
|
|
365
360
|
})
|
|
366
361
|
|
|
@@ -426,3 +421,35 @@ test('code block immediately after list marker', () => {
|
|
|
426
421
|
2. Normal item"
|
|
427
422
|
`)
|
|
428
423
|
})
|
|
424
|
+
|
|
425
|
+
test('code block with filename metadata', () => {
|
|
426
|
+
const input = `- Item with code
|
|
427
|
+
\`\`\`tsx filename=example.tsx
|
|
428
|
+
const x = 1
|
|
429
|
+
\`\`\``
|
|
430
|
+
const result = unnestCodeBlocksFromLists(input)
|
|
431
|
+
expect(result).toMatchInlineSnapshot(`
|
|
432
|
+
"- Item with code
|
|
433
|
+
|
|
434
|
+
\`\`\`tsx filename=example.tsx
|
|
435
|
+
const x = 1
|
|
436
|
+
\`\`\`"
|
|
437
|
+
`)
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
test('numbered list with filename metadata code block', () => {
|
|
441
|
+
const input = `1. First item
|
|
442
|
+
\`\`\`tsx filename=app.tsx
|
|
443
|
+
export default function App() {}
|
|
444
|
+
\`\`\`
|
|
445
|
+
2. Second item`
|
|
446
|
+
const result = unnestCodeBlocksFromLists(input)
|
|
447
|
+
expect(result).toMatchInlineSnapshot(`
|
|
448
|
+
"1. First item
|
|
449
|
+
|
|
450
|
+
\`\`\`tsx filename=app.tsx
|
|
451
|
+
export default function App() {}
|
|
452
|
+
\`\`\`
|
|
453
|
+
2. Second item"
|
|
454
|
+
`)
|
|
455
|
+
})
|
|
@@ -123,10 +123,12 @@ function renderSegments(segments: Segment[]): string {
|
|
|
123
123
|
result.push(segment.prefix + segment.content + '\n')
|
|
124
124
|
} else {
|
|
125
125
|
// Raw content (no prefix means it's original raw)
|
|
126
|
-
|
|
126
|
+
// Ensure raw ends with newline for proper separation from next segment
|
|
127
|
+
const raw = segment.content.trimEnd()
|
|
128
|
+
result.push(raw + '\n')
|
|
127
129
|
}
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
return result.join('')
|
|
133
|
+
return result.join('').trimEnd()
|
|
132
134
|
}
|