remote-opencode 1.0.8 β†’ 1.0.10

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.
@@ -1,11 +1,8 @@
1
- import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags, EmbedBuilder } from 'discord.js';
1
+ import { SlashCommandBuilder, MessageFlags } from 'discord.js';
2
2
  import * as dataStore from '../services/dataStore.js';
3
- import * as sessionManager from '../services/sessionManager.js';
4
- import * as serveManager from '../services/serveManager.js';
5
- import * as worktreeManager from '../services/worktreeManager.js';
6
- import { SSEClient } from '../services/sseClient.js';
7
3
  import { getOrCreateThread } from '../utils/threadHelper.js';
8
- import { formatOutput } from '../utils/messageFormatter.js';
4
+ import { runPrompt } from '../services/executionService.js';
5
+ import { isBusy } from '../services/queueManager.js';
9
6
  function getParentChannelId(interaction) {
10
7
  const channel = interaction.channel;
11
8
  if (channel?.isThread()) {
@@ -49,50 +46,14 @@ export const opencode = {
49
46
  return;
50
47
  }
51
48
  const threadId = thread.id;
52
- // Auto-create worktree if enabled and this is a new thread (not isInThread)
53
- let worktreeMapping = dataStore.getWorktreeMapping(threadId);
54
- if (!worktreeMapping && !isInThread) {
55
- const projectAlias = dataStore.getChannelBinding(channelId);
56
- if (projectAlias && dataStore.getProjectAutoWorktree(projectAlias)) {
57
- try {
58
- const branchName = worktreeManager.sanitizeBranchName(`auto/${threadId.slice(0, 8)}-${Date.now()}`);
59
- const worktreePath = await worktreeManager.createWorktree(projectPath, branchName);
60
- const newMapping = {
61
- threadId,
62
- branchName,
63
- worktreePath,
64
- projectPath,
65
- description: prompt.slice(0, 50) + (prompt.length > 50 ? '...' : ''),
66
- createdAt: Date.now()
67
- };
68
- dataStore.setWorktreeMapping(newMapping);
69
- worktreeMapping = newMapping;
70
- const embed = new EmbedBuilder()
71
- .setTitle(`🌳 Auto-Worktree: ${branchName}`)
72
- .setDescription('Automatically created for this session')
73
- .addFields({ name: 'Branch', value: branchName, inline: true }, { name: 'Path', value: worktreePath, inline: true })
74
- .setColor(0x2ecc71);
75
- const worktreeButtons = new ActionRowBuilder()
76
- .addComponents(new ButtonBuilder()
77
- .setCustomId(`delete_${threadId}`)
78
- .setLabel('Delete')
79
- .setStyle(ButtonStyle.Danger), new ButtonBuilder()
80
- .setCustomId(`pr_${threadId}`)
81
- .setLabel('Create PR')
82
- .setStyle(ButtonStyle.Primary));
83
- await thread.send({ embeds: [embed], components: [worktreeButtons] });
84
- }
85
- catch (error) {
86
- console.error('Auto-worktree creation failed:', error);
87
- // Continue with main project path (graceful degradation)
88
- }
89
- }
90
- }
91
- const effectivePath = worktreeMapping?.worktreePath ?? projectPath;
92
- const existingClient = sessionManager.getSseClient(threadId);
93
- if (existingClient && existingClient.isConnected()) {
49
+ if (isBusy(threadId)) {
50
+ dataStore.addToQueue(threadId, {
51
+ prompt,
52
+ userId: interaction.user.id,
53
+ timestamp: Date.now()
54
+ });
94
55
  await interaction.reply({
95
- content: '⚠️ Already running. Wait for completion or press [Interrupt].',
56
+ content: 'πŸ“₯ Prompt added to queue.',
96
57
  flags: MessageFlags.Ephemeral
97
58
  });
98
59
  return;
@@ -100,128 +61,6 @@ export const opencode = {
100
61
  await interaction.reply({
101
62
  content: `πŸ“Œ **Prompt**: ${prompt}`
102
63
  });
103
- const buttons = new ActionRowBuilder()
104
- .addComponents(new ButtonBuilder()
105
- .setCustomId(`interrupt_${threadId}`)
106
- .setLabel('⏸️ Interrupt')
107
- .setStyle(ButtonStyle.Secondary));
108
- let streamMessage;
109
- try {
110
- streamMessage = await thread.send({
111
- content: 'πŸš€ Starting OpenCode server...',
112
- components: [buttons]
113
- });
114
- }
115
- catch {
116
- await interaction.editReply({
117
- content: `πŸ“Œ **Prompt**: ${prompt}\n\n❌ Cannot send message to thread.`
118
- });
119
- return;
120
- }
121
- let port;
122
- let sessionId;
123
- let updateInterval = null;
124
- let accumulatedText = '';
125
- let lastContent = '';
126
- let tick = 0;
127
- const spinner = ['β ‹', 'β ™', 'β Ή', 'β Έ', 'β Ό', 'β ΄', 'β ¦', 'β §', 'β ‡', '⠏'];
128
- const updateStreamMessage = async (content, components) => {
129
- try {
130
- await streamMessage.edit({ content, components });
131
- }
132
- catch {
133
- }
134
- };
135
- try {
136
- port = await serveManager.spawnServe(effectivePath);
137
- await updateStreamMessage('⏳ Waiting for OpenCode server...', [buttons]);
138
- await serveManager.waitForReady(port);
139
- const existingSession = sessionManager.getSessionForThread(threadId);
140
- if (existingSession && existingSession.projectPath === effectivePath) {
141
- const isValid = await sessionManager.validateSession(port, existingSession.sessionId);
142
- if (isValid) {
143
- sessionId = existingSession.sessionId;
144
- sessionManager.updateSessionLastUsed(threadId);
145
- }
146
- else {
147
- sessionId = await sessionManager.createSession(port);
148
- sessionManager.setSessionForThread(threadId, sessionId, effectivePath, port);
149
- }
150
- }
151
- else {
152
- sessionId = await sessionManager.createSession(port);
153
- sessionManager.setSessionForThread(threadId, sessionId, effectivePath, port);
154
- }
155
- const sseClient = new SSEClient();
156
- sseClient.connect(`http://localhost:${port}`);
157
- sessionManager.setSseClient(threadId, sseClient);
158
- sseClient.onPartUpdated((part) => {
159
- accumulatedText = part.text;
160
- });
161
- sseClient.onSessionIdle(() => {
162
- if (updateInterval) {
163
- clearInterval(updateInterval);
164
- updateInterval = null;
165
- }
166
- (async () => {
167
- try {
168
- const formatted = formatOutput(accumulatedText);
169
- const disabledButtons = new ActionRowBuilder()
170
- .addComponents(new ButtonBuilder()
171
- .setCustomId(`interrupt_${threadId}`)
172
- .setLabel('⏸️ Interrupt')
173
- .setStyle(ButtonStyle.Secondary)
174
- .setDisabled(true));
175
- await updateStreamMessage(`\`\`\`\n${formatted}\n\`\`\``, [disabledButtons]);
176
- await thread.send({ content: 'βœ… Done' });
177
- sseClient.disconnect();
178
- sessionManager.clearSseClient(threadId);
179
- }
180
- catch {
181
- }
182
- })();
183
- });
184
- sseClient.onError((error) => {
185
- if (updateInterval) {
186
- clearInterval(updateInterval);
187
- updateInterval = null;
188
- }
189
- (async () => {
190
- try {
191
- await updateStreamMessage(`❌ Connection error: ${error.message}`, []);
192
- }
193
- catch {
194
- }
195
- })();
196
- });
197
- updateInterval = setInterval(async () => {
198
- tick++;
199
- try {
200
- const formatted = formatOutput(accumulatedText);
201
- const spinnerChar = spinner[tick % spinner.length];
202
- const newContent = formatted || 'Processing...';
203
- if (newContent !== lastContent || tick % 2 === 0) {
204
- lastContent = newContent;
205
- await updateStreamMessage(`${spinnerChar} **Running...**\n\`\`\`\n${newContent}\n\`\`\``, [buttons]);
206
- }
207
- }
208
- catch {
209
- }
210
- }, 1000);
211
- await updateStreamMessage('πŸ“ Sending prompt...', [buttons]);
212
- await sessionManager.sendPrompt(port, sessionId, prompt);
213
- }
214
- catch (error) {
215
- if (updateInterval) {
216
- clearInterval(updateInterval);
217
- }
218
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
219
- await updateStreamMessage(`❌ OpenCode execution failed: ${errorMessage}`, []);
220
- const client = sessionManager.getSseClient(threadId);
221
- if (client) {
222
- client.disconnect();
223
- sessionManager.clearSseClient(threadId);
224
- }
225
- }
64
+ await runPrompt(thread, threadId, prompt, channelId);
226
65
  }
227
66
  };
@@ -0,0 +1,85 @@
1
+ import { SlashCommandBuilder, MessageFlags } from 'discord.js';
2
+ import * as dataStore from '../services/dataStore.js';
3
+ import { processNextInQueue } from '../services/queueManager.js';
4
+ export const queue = {
5
+ data: new SlashCommandBuilder()
6
+ .setName('queue')
7
+ .setDescription('Manage the job queue for this thread')
8
+ .addSubcommand(sub => sub.setName('list')
9
+ .setDescription('List all pending prompts in the queue'))
10
+ .addSubcommand(sub => sub.setName('clear')
11
+ .setDescription('Clear all pending prompts in the queue'))
12
+ .addSubcommand(sub => sub.setName('pause')
13
+ .setDescription('Pause the queue (current job will finish)'))
14
+ .addSubcommand(sub => sub.setName('resume')
15
+ .setDescription('Resume the queue and start next task if idle'))
16
+ .addSubcommand(sub => sub.setName('settings')
17
+ .setDescription('Configure queue behavior')
18
+ .addBooleanOption(opt => opt.setName('continue_on_failure')
19
+ .setDescription('Whether to continue to next task if current one fails'))
20
+ .addBooleanOption(opt => opt.setName('fresh_context')
21
+ .setDescription('Whether to clear AI conversation context between tasks'))),
22
+ async execute(interaction) {
23
+ const thread = interaction.channel;
24
+ if (!thread?.isThread()) {
25
+ await interaction.reply({
26
+ content: '❌ This command can only be used in a thread.',
27
+ flags: MessageFlags.Ephemeral
28
+ });
29
+ return;
30
+ }
31
+ const threadId = thread.id;
32
+ const subcommand = interaction.options.getSubcommand();
33
+ if (subcommand === 'list') {
34
+ const q = dataStore.getQueue(threadId);
35
+ if (q.length === 0) {
36
+ await interaction.reply({ content: 'πŸ“­ The queue is empty.', flags: MessageFlags.Ephemeral });
37
+ return;
38
+ }
39
+ const list = q.map((m, i) => `${i + 1}. ${m.prompt.slice(0, 100)}${m.prompt.length > 100 ? '...' : ''}`).join('\n');
40
+ const settings = dataStore.getQueueSettings(threadId);
41
+ const status = settings.paused ? '⏸️ Paused' : '▢️ Running';
42
+ await interaction.reply({
43
+ content: `πŸ“‹ **Queue Status**: ${status}\n\n**Pending Tasks**:\n${list}`,
44
+ flags: MessageFlags.Ephemeral
45
+ });
46
+ }
47
+ else if (subcommand === 'clear') {
48
+ dataStore.clearQueue(threadId);
49
+ await interaction.reply({ content: 'πŸ—‘οΈ Queue cleared.', flags: MessageFlags.Ephemeral });
50
+ }
51
+ else if (subcommand === 'pause') {
52
+ dataStore.updateQueueSettings(threadId, { paused: true });
53
+ await interaction.reply({ content: '⏸️ Queue paused. Current job will finish, but next one won\'t start automatically.', flags: MessageFlags.Ephemeral });
54
+ }
55
+ else if (subcommand === 'resume') {
56
+ dataStore.updateQueueSettings(threadId, { paused: false });
57
+ await interaction.reply({ content: '▢️ Queue resumed.', flags: MessageFlags.Ephemeral });
58
+ // Try to trigger next if idle
59
+ const parentChannelId = thread.parentId;
60
+ if (parentChannelId) {
61
+ const sseClient = (await import('../services/sessionManager.js')).getSseClient(threadId);
62
+ if (!sseClient || !sseClient.isConnected()) {
63
+ await processNextInQueue(thread, threadId, parentChannelId);
64
+ }
65
+ }
66
+ }
67
+ else if (subcommand === 'settings') {
68
+ const continueOnFailure = interaction.options.getBoolean('continue_on_failure');
69
+ const freshContext = interaction.options.getBoolean('fresh_context');
70
+ const updates = {};
71
+ if (continueOnFailure !== null)
72
+ updates.continueOnFailure = continueOnFailure;
73
+ if (freshContext !== null)
74
+ updates.freshContext = freshContext;
75
+ if (Object.keys(updates).length > 0) {
76
+ dataStore.updateQueueSettings(threadId, updates);
77
+ }
78
+ const settings = dataStore.getQueueSettings(threadId);
79
+ await interaction.reply({
80
+ content: `βš™οΈ **Queue Settings Updated**:\n- Continue on failure: \`${settings.continueOnFailure}\`\n- Fresh context: \`${settings.freshContext}\``,
81
+ flags: MessageFlags.Ephemeral
82
+ });
83
+ }
84
+ }
85
+ };
@@ -0,0 +1,33 @@
1
+ import { SlashCommandBuilder } from 'discord.js';
2
+ import { setPortConfig } from '../services/configStore.js';
3
+ export const setports = {
4
+ data: new SlashCommandBuilder()
5
+ .setName('setports')
6
+ .setDescription('Configure the port range for OpenCode server instances')
7
+ .addIntegerOption(option => option.setName('min')
8
+ .setDescription('Minimum port number')
9
+ .setRequired(true))
10
+ .addIntegerOption(option => option.setName('max')
11
+ .setDescription('Maximum port number')
12
+ .setRequired(true)),
13
+ async execute(interaction) {
14
+ const min = interaction.options.getInteger('min', true);
15
+ const max = interaction.options.getInteger('max', true);
16
+ if (min >= max) {
17
+ await interaction.reply({
18
+ content: '❌ Minimum port must be less than maximum port.',
19
+ ephemeral: true
20
+ });
21
+ return;
22
+ }
23
+ if (min < 1024 || max > 65535) {
24
+ await interaction.reply({
25
+ content: '❌ Ports must be between 1024 and 65535.',
26
+ ephemeral: true
27
+ });
28
+ return;
29
+ }
30
+ setPortConfig({ min, max });
31
+ await interaction.reply(`βœ… Port range updated: ${min}-${max}`);
32
+ }
33
+ };
@@ -1,3 +1,4 @@
1
+ import { MessageFlags } from 'discord.js';
1
2
  import * as sessionManager from '../services/sessionManager.js';
2
3
  import * as serveManager from '../services/serveManager.js';
3
4
  import * as dataStore from '../services/dataStore.js';
@@ -8,7 +9,7 @@ export async function handleButton(interaction) {
8
9
  if (!threadId) {
9
10
  await interaction.reply({
10
11
  content: '❌ Invalid button.',
11
- ephemeral: true
12
+ flags: MessageFlags.Ephemeral
12
13
  });
13
14
  return;
14
15
  }
@@ -24,7 +25,7 @@ export async function handleButton(interaction) {
24
25
  else {
25
26
  await interaction.reply({
26
27
  content: '❌ Unknown action.',
27
- ephemeral: true
28
+ flags: MessageFlags.Ephemeral
28
29
  });
29
30
  }
30
31
  }
@@ -33,19 +34,22 @@ async function handleInterrupt(interaction, threadId) {
33
34
  if (!session) {
34
35
  await interaction.reply({
35
36
  content: '⚠️ Session not found.',
36
- ephemeral: true
37
+ flags: MessageFlags.Ephemeral
37
38
  });
38
39
  return;
39
40
  }
40
- const port = serveManager.getPort(session.projectPath);
41
+ const channel = interaction.channel;
42
+ const parentChannelId = channel?.isThread() ? channel.parentId : channel?.id;
43
+ const preferredModel = parentChannelId ? dataStore.getChannelModel(parentChannelId) : undefined;
44
+ const port = serveManager.getPort(session.projectPath, preferredModel);
41
45
  if (!port) {
42
46
  await interaction.reply({
43
47
  content: '⚠️ Server is not running.',
44
- ephemeral: true
48
+ flags: MessageFlags.Ephemeral
45
49
  });
46
50
  return;
47
51
  }
48
- await interaction.deferReply({ ephemeral: true });
52
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
49
53
  const success = await sessionManager.abortSession(port, session.sessionId);
50
54
  if (success) {
51
55
  await interaction.editReply({ content: '⏸️ Interrupt request sent.' });
@@ -57,10 +61,10 @@ async function handleInterrupt(interaction, threadId) {
57
61
  async function handleWorktreeDelete(interaction, threadId) {
58
62
  const mapping = dataStore.getWorktreeMapping(threadId);
59
63
  if (!mapping) {
60
- await interaction.reply({ content: '⚠️ Worktree mapping not found.', ephemeral: true });
64
+ await interaction.reply({ content: '⚠️ Worktree mapping not found.', flags: MessageFlags.Ephemeral });
61
65
  return;
62
66
  }
63
- await interaction.deferReply({ ephemeral: true });
67
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
64
68
  try {
65
69
  if (worktreeManager.worktreeExists(mapping.worktreePath)) {
66
70
  await worktreeManager.removeWorktree(mapping.worktreePath, false);
@@ -79,13 +83,16 @@ async function handleWorktreeDelete(interaction, threadId) {
79
83
  async function handleWorktreePR(interaction, threadId) {
80
84
  const mapping = dataStore.getWorktreeMapping(threadId);
81
85
  if (!mapping) {
82
- await interaction.reply({ content: '⚠️ Worktree mapping not found.', ephemeral: true });
86
+ await interaction.reply({ content: '⚠️ Worktree mapping not found.', flags: MessageFlags.Ephemeral });
83
87
  return;
84
88
  }
85
- await interaction.deferReply({ ephemeral: true });
89
+ const channel = interaction.channel;
90
+ const parentChannelId = channel?.isThread() ? channel.parentId : channel?.id;
91
+ const preferredModel = parentChannelId ? dataStore.getChannelModel(parentChannelId) : undefined;
92
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
86
93
  try {
87
- const port = await serveManager.spawnServe(mapping.worktreePath);
88
- await serveManager.waitForReady(port);
94
+ const port = await serveManager.spawnServe(mapping.worktreePath, preferredModel);
95
+ await serveManager.waitForReady(port, 30000, mapping.worktreePath, preferredModel);
89
96
  let sessionId;
90
97
  const existingSession = sessionManager.getSessionForThread(threadId);
91
98
  if (existingSession) {
@@ -97,7 +104,7 @@ async function handleWorktreePR(interaction, threadId) {
97
104
  }
98
105
  sessionManager.setSessionForThread(threadId, sessionId, mapping.worktreePath, port);
99
106
  const prPrompt = `Create a pull request for the current branch. Include a clear title and description summarizing all changes.`;
100
- await sessionManager.sendPrompt(port, sessionId, prPrompt);
107
+ await sessionManager.sendPrompt(port, sessionId, prPrompt, preferredModel);
101
108
  await interaction.editReply({ content: 'πŸš€ PR creation started! Check the thread for progress.' });
102
109
  }
103
110
  catch (error) {
@@ -1,8 +1,14 @@
1
+ import { MessageFlags } from 'discord.js';
1
2
  import { commands } from '../commands/index.js';
2
3
  import { handleButton } from './buttonHandler.js';
3
4
  export async function handleInteraction(interaction) {
4
5
  if (interaction.isButton()) {
5
- await handleButton(interaction);
6
+ try {
7
+ await handleButton(interaction);
8
+ }
9
+ catch (error) {
10
+ console.error('Error handling button:', error);
11
+ }
6
12
  return;
7
13
  }
8
14
  if (!interaction.isChatInputCommand())
@@ -15,12 +21,18 @@ export async function handleInteraction(interaction) {
15
21
  await command.execute(interaction);
16
22
  }
17
23
  catch (error) {
24
+ console.error(`Error executing command ${interaction.commandName}:`, error);
18
25
  const content = '❌ An error occurred while executing the command.';
19
- if (interaction.replied || interaction.deferred) {
20
- await interaction.followUp({ content, ephemeral: true });
26
+ try {
27
+ if (interaction.replied || interaction.deferred) {
28
+ await interaction.followUp({ content, flags: MessageFlags.Ephemeral });
29
+ }
30
+ else {
31
+ await interaction.reply({ content, flags: MessageFlags.Ephemeral });
32
+ }
21
33
  }
22
- else {
23
- await interaction.reply({ content, ephemeral: true });
34
+ catch (replyError) {
35
+ console.error('Failed to send error response to user:', replyError);
24
36
  }
25
37
  }
26
38
  }
@@ -1,10 +1,6 @@
1
- import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js';
2
1
  import * as dataStore from '../services/dataStore.js';
3
- import * as sessionManager from '../services/sessionManager.js';
4
- import * as serveManager from '../services/serveManager.js';
5
- import * as worktreeManager from '../services/worktreeManager.js';
6
- import { SSEClient } from '../services/sseClient.js';
7
- import { formatOutput } from '../utils/messageFormatter.js';
2
+ import { runPrompt } from '../services/executionService.js';
3
+ import { isBusy } from '../services/queueManager.js';
8
4
  export async function handleMessageCreate(message) {
9
5
  if (message.author.bot)
10
6
  return;
@@ -19,178 +15,17 @@ export async function handleMessageCreate(message) {
19
15
  const parentChannelId = channel.parentId;
20
16
  if (!parentChannelId)
21
17
  return;
22
- const projectPath = dataStore.getChannelProjectPath(parentChannelId);
23
- if (!projectPath) {
24
- await message.reply('❌ No project bound to parent channel. Disable passthrough and use `/use` first.');
25
- return;
26
- }
27
- let worktreeMapping = dataStore.getWorktreeMapping(threadId);
28
- // Auto-create worktree if enabled and no mapping exists for this thread
29
- if (!worktreeMapping) {
30
- const projectAlias = dataStore.getChannelBinding(parentChannelId);
31
- if (projectAlias && dataStore.getProjectAutoWorktree(projectAlias)) {
32
- try {
33
- const branchName = worktreeManager.sanitizeBranchName(`auto/${threadId.slice(0, 8)}-${Date.now()}`);
34
- const worktreePath = await worktreeManager.createWorktree(projectPath, branchName);
35
- const prompt = message.content.trim();
36
- const newMapping = {
37
- threadId,
38
- branchName,
39
- worktreePath,
40
- projectPath,
41
- description: prompt.slice(0, 50) + (prompt.length > 50 ? '...' : ''),
42
- createdAt: Date.now()
43
- };
44
- dataStore.setWorktreeMapping(newMapping);
45
- worktreeMapping = newMapping;
46
- const embed = new EmbedBuilder()
47
- .setTitle(`🌳 Auto-Worktree: ${branchName}`)
48
- .setDescription('Automatically created for this session')
49
- .addFields({ name: 'Branch', value: branchName, inline: true }, { name: 'Path', value: worktreePath, inline: true })
50
- .setColor(0x2ecc71);
51
- const worktreeButtons = new ActionRowBuilder()
52
- .addComponents(new ButtonBuilder()
53
- .setCustomId(`delete_${threadId}`)
54
- .setLabel('Delete')
55
- .setStyle(ButtonStyle.Danger), new ButtonBuilder()
56
- .setCustomId(`pr_${threadId}`)
57
- .setLabel('Create PR')
58
- .setStyle(ButtonStyle.Primary));
59
- await channel.send({ embeds: [embed], components: [worktreeButtons] });
60
- }
61
- catch (error) {
62
- console.error('Auto-worktree creation failed:', error);
63
- // Continue with main project path (graceful degradation)
64
- }
65
- }
66
- }
67
- const effectivePath = worktreeMapping?.worktreePath ?? projectPath;
68
- const existingClient = sessionManager.getSseClient(threadId);
69
- if (existingClient && existingClient.isConnected()) {
70
- await message.react('⏳');
71
- return;
72
- }
73
18
  const prompt = message.content.trim();
74
19
  if (!prompt)
75
20
  return;
76
- const buttons = new ActionRowBuilder()
77
- .addComponents(new ButtonBuilder()
78
- .setCustomId(`interrupt_${threadId}`)
79
- .setLabel('⏸️ Interrupt')
80
- .setStyle(ButtonStyle.Secondary));
81
- let streamMessage;
82
- try {
83
- streamMessage = await channel.send({
84
- content: `πŸ“Œ **Prompt**: ${prompt}\n\nπŸš€ Starting OpenCode server...`,
85
- components: [buttons]
21
+ if (isBusy(threadId)) {
22
+ dataStore.addToQueue(threadId, {
23
+ prompt,
24
+ userId: message.author.id,
25
+ timestamp: Date.now()
86
26
  });
87
- }
88
- catch {
27
+ await message.react('πŸ“₯');
89
28
  return;
90
29
  }
91
- let port;
92
- let sessionId;
93
- let updateInterval = null;
94
- let accumulatedText = '';
95
- let lastContent = '';
96
- let tick = 0;
97
- const spinner = ['β ‹', 'β ™', 'β Ή', 'β Έ', 'β Ό', 'β ΄', 'β ¦', 'β §', 'β ‡', '⠏'];
98
- const updateStreamMessage = async (content, components) => {
99
- try {
100
- await streamMessage.edit({ content, components });
101
- }
102
- catch {
103
- }
104
- };
105
- try {
106
- port = await serveManager.spawnServe(effectivePath);
107
- await updateStreamMessage(`πŸ“Œ **Prompt**: ${prompt}\n\n⏳ Waiting for OpenCode server...`, [buttons]);
108
- await serveManager.waitForReady(port);
109
- const existingSession = sessionManager.getSessionForThread(threadId);
110
- if (existingSession && existingSession.projectPath === effectivePath) {
111
- const isValid = await sessionManager.validateSession(port, existingSession.sessionId);
112
- if (isValid) {
113
- sessionId = existingSession.sessionId;
114
- sessionManager.updateSessionLastUsed(threadId);
115
- }
116
- else {
117
- sessionId = await sessionManager.createSession(port);
118
- sessionManager.setSessionForThread(threadId, sessionId, effectivePath, port);
119
- }
120
- }
121
- else {
122
- sessionId = await sessionManager.createSession(port);
123
- sessionManager.setSessionForThread(threadId, sessionId, effectivePath, port);
124
- }
125
- const sseClient = new SSEClient();
126
- sseClient.connect(`http://localhost:${port}`);
127
- sessionManager.setSseClient(threadId, sseClient);
128
- sseClient.onPartUpdated((part) => {
129
- accumulatedText = part.text;
130
- });
131
- sseClient.onSessionIdle(() => {
132
- if (updateInterval) {
133
- clearInterval(updateInterval);
134
- updateInterval = null;
135
- }
136
- (async () => {
137
- try {
138
- const formatted = formatOutput(accumulatedText);
139
- const disabledButtons = new ActionRowBuilder()
140
- .addComponents(new ButtonBuilder()
141
- .setCustomId(`interrupt_${threadId}`)
142
- .setLabel('⏸️ Interrupt')
143
- .setStyle(ButtonStyle.Secondary)
144
- .setDisabled(true));
145
- await updateStreamMessage(`πŸ“Œ **Prompt**: ${prompt}\n\n\`\`\`\n${formatted}\n\`\`\``, [disabledButtons]);
146
- await channel.send({ content: 'βœ… Done' });
147
- sseClient.disconnect();
148
- sessionManager.clearSseClient(threadId);
149
- }
150
- catch {
151
- }
152
- })();
153
- });
154
- sseClient.onError((error) => {
155
- if (updateInterval) {
156
- clearInterval(updateInterval);
157
- updateInterval = null;
158
- }
159
- (async () => {
160
- try {
161
- await updateStreamMessage(`πŸ“Œ **Prompt**: ${prompt}\n\n❌ Connection error: ${error.message}`, []);
162
- }
163
- catch {
164
- }
165
- })();
166
- });
167
- updateInterval = setInterval(async () => {
168
- tick++;
169
- try {
170
- const formatted = formatOutput(accumulatedText);
171
- const spinnerChar = spinner[tick % spinner.length];
172
- const newContent = formatted || 'Processing...';
173
- if (newContent !== lastContent || tick % 2 === 0) {
174
- lastContent = newContent;
175
- await updateStreamMessage(`πŸ“Œ **Prompt**: ${prompt}\n\n${spinnerChar} **Running...**\n\`\`\`\n${newContent}\n\`\`\``, [buttons]);
176
- }
177
- }
178
- catch {
179
- }
180
- }, 1000);
181
- await updateStreamMessage(`πŸ“Œ **Prompt**: ${prompt}\n\nπŸ“ Sending prompt...`, [buttons]);
182
- await sessionManager.sendPrompt(port, sessionId, prompt);
183
- }
184
- catch (error) {
185
- if (updateInterval) {
186
- clearInterval(updateInterval);
187
- }
188
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
189
- await updateStreamMessage(`πŸ“Œ **Prompt**: ${prompt}\n\n❌ OpenCode execution failed: ${errorMessage}`, []);
190
- const client = sessionManager.getSseClient(threadId);
191
- if (client) {
192
- client.disconnect();
193
- sessionManager.clearSseClient(threadId);
194
- }
195
- }
30
+ await runPrompt(channel, threadId, prompt, parentChannelId);
196
31
  }
@@ -36,6 +36,14 @@ export function setBotConfig(bot) {
36
36
  config.bot = bot;
37
37
  saveConfig(config);
38
38
  }
39
+ export function getPortConfig() {
40
+ return loadConfig().ports;
41
+ }
42
+ export function setPortConfig(ports) {
43
+ const config = loadConfig();
44
+ config.ports = ports;
45
+ saveConfig(config);
46
+ }
39
47
  export function hasBotConfig() {
40
48
  const bot = getBotConfig();
41
49
  return !!(bot?.discordToken && bot?.clientId && bot?.guildId);