remote-opencode 1.0.1 β 1.0.3
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/LICENSE +21 -0
- package/README.md +99 -22
- package/dist/src/bot.js +7 -1
- package/dist/src/commands/code.js +47 -0
- package/dist/src/commands/index.js +2 -0
- package/dist/src/handlers/messageHandler.js +156 -0
- package/dist/src/services/dataStore.js +43 -0
- package/package.json +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RoundTable02
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> Control your AI coding assistant from anywhere β your phone, tablet, or another computer.
|
|
4
4
|
|
|
5
|
+
<div align="center">
|
|
6
|
+
<img width="1024" alt="Gemini_Generated_Image_47d5gq47d5gq47d5" src="https://github.com/user-attachments/assets/1defa11d-6195-4a9c-956b-4f87470f6393" />
|
|
7
|
+
</div>
|
|
8
|
+
|
|
5
9
|
**remote-opencode** is a Discord bot that bridges your local [OpenCode CLI](https://github.com/sst/opencode) to Discord, enabling you to interact with your AI coding assistant remotely. Perfect for developers who want to:
|
|
6
10
|
|
|
7
11
|
- π± **Code from mobile** β Send coding tasks from your phone while away from your desk
|
|
@@ -38,6 +42,12 @@ The bot runs on your development machine alongside OpenCode. When you send a com
|
|
|
38
42
|
- [Installation](#installation)
|
|
39
43
|
- [Quick Start](#quick-start)
|
|
40
44
|
- [Discord Bot Setup](#discord-bot-setup)
|
|
45
|
+
- [Step 1: Create Discord Application](#step-1-create-discord-application)
|
|
46
|
+
- [Step 2: Create Bot & Get Token](#step-2-create-bot--get-token)
|
|
47
|
+
- [Step 3: Enable Required Intents](#step-3-enable-required-intents)
|
|
48
|
+
- [Step 4: Configure Bot Permissions](#step-4-configure-bot-permissions)
|
|
49
|
+
- [Step 5: Get Your Server (Guild) ID](#step-5-get-your-server-guild-id)
|
|
50
|
+
- [Step 6: Invite Bot to Your Server](#step-6-invite-bot-to-your-server)
|
|
41
51
|
- [CLI Commands](#cli-commands)
|
|
42
52
|
- [Discord Slash Commands](#discord-slash-commands)
|
|
43
53
|
- [Usage Workflow](#usage-workflow)
|
|
@@ -69,7 +79,7 @@ npx remote-opencode
|
|
|
69
79
|
### Install from source
|
|
70
80
|
|
|
71
81
|
```bash
|
|
72
|
-
git clone https://github.com
|
|
82
|
+
git clone https://github.com/RoundTable02/remote-opencode.git
|
|
73
83
|
cd remote-opencode
|
|
74
84
|
npm install
|
|
75
85
|
npm run build
|
|
@@ -103,9 +113,7 @@ Before using remote-opencode, you need to create a Discord Application and Bot.
|
|
|
103
113
|
3. Enter a name (e.g., "Remote OpenCode")
|
|
104
114
|
4. Copy the **Application ID** β you'll need this later
|
|
105
115
|
|
|
106
|
-
|
|
107
|
-
<!--  -->
|
|
108
|
-
*Screenshot placeholder: Discord Developer Portal showing "New Application" button*
|
|
116
|
+
<img width="800" alt="image" src="https://github.com/user-attachments/assets/ca2e7ff3-91e7-4d66-93dc-c166189c0107" />
|
|
109
117
|
|
|
110
118
|
### Step 2: Create Bot & Get Token
|
|
111
119
|
|
|
@@ -114,9 +122,6 @@ Before using remote-opencode, you need to create a Discord Application and Bot.
|
|
|
114
122
|
3. **Copy the token immediately** β it's only shown once!
|
|
115
123
|
4. Keep this token secret β never share it publicly
|
|
116
124
|
|
|
117
|
-
<!-- SCREENSHOT: Bot section showing Reset Token button -->
|
|
118
|
-
<!--  -->
|
|
119
|
-
*Screenshot placeholder: Bot settings page with token section highlighted*
|
|
120
125
|
|
|
121
126
|
### Step 3: Enable Required Intents
|
|
122
127
|
|
|
@@ -127,36 +132,67 @@ Still in the **"Bot"** section, scroll down to **"Privileged Gateway Intents"**
|
|
|
127
132
|
|
|
128
133
|
Click **"Save Changes"**
|
|
129
134
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
<img width="1500" alt="image" src="https://github.com/user-attachments/assets/d20406ff-26ad-4204-9771-b157c340846a" />
|
|
136
|
+
|
|
137
|
+
### Step 4: Configure Bot Permissions
|
|
138
|
+
|
|
139
|
+
The bot needs specific permissions to function properly. You can configure permissions in two ways:
|
|
140
|
+
|
|
141
|
+
#### Option A: Using OAuth2 URL Generator (Recommended)
|
|
142
|
+
|
|
143
|
+
1. Navigate to the **"OAuth2"** section in the sidebar
|
|
144
|
+
2. Click on **"URL Generator"**
|
|
145
|
+
3. In **"Scopes"**, select:
|
|
146
|
+
- β
`bot`
|
|
147
|
+
- β
`applications.commands`
|
|
148
|
+
4. In **"Bot Permissions"**, select only these essential permissions:
|
|
149
|
+
|
|
150
|
+
**General Permissions:**
|
|
151
|
+
- β
**View Channels** β Required to access channels
|
|
152
|
+
|
|
153
|
+
**Text Permissions:**
|
|
154
|
+
- β
**Send Messages** β Send responses to channels
|
|
155
|
+
- β
**Create Public Threads** β Create threads for each `/opencode` session
|
|
156
|
+
- β
**Send Messages in Threads** β Reply within threads
|
|
157
|
+
- β
**Embed Links** β Send formatted embed messages
|
|
158
|
+
- β
**Read Message History** β Access context for conversations
|
|
159
|
+
- β
**Add Reactions** β Add buttons (uses emoji reactions internally)
|
|
160
|
+
- β
**Use Slash Commands** β Register and use slash commands
|
|
161
|
+
|
|
162
|
+
5. Copy the generated URL at the bottom β this is your bot invite link!
|
|
163
|
+
|
|
133
164
|
|
|
134
|
-
|
|
165
|
+
#### Option B: Manual Permission Calculation
|
|
166
|
+
|
|
167
|
+
If you're building the URL manually, use this permission value:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=311385214016&integration_type=0&scope=bot+applications.commands
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Important:** The URL must include `applications.commands` scope for slash commands to work!
|
|
174
|
+
|
|
175
|
+
### Step 5: Get Your Server (Guild) ID
|
|
135
176
|
|
|
136
177
|
1. Open Discord and go to **User Settings β Advanced**
|
|
137
178
|
2. Enable **"Developer Mode"**
|
|
138
179
|
3. Right-click on your server name in the sidebar
|
|
139
180
|
4. Click **"Copy Server ID"**
|
|
140
181
|
|
|
141
|
-
|
|
142
|
-
<!--  -->
|
|
143
|
-
*Screenshot placeholder: Right-click menu showing "Copy Server ID" option*
|
|
182
|
+
<img width="184" height="530" alt="αα
³αα
³α
α
΅α«αα
£αΊ 2026-02-03 αα
©αα
₯α« 2 34 31" src="https://github.com/user-attachments/assets/8ecc2a28-05e5-494f-834f-95d9d0e4e730" />
|
|
144
183
|
|
|
145
|
-
### Step
|
|
184
|
+
### Step 6: Invite Bot to Your Server
|
|
146
185
|
|
|
147
|
-
|
|
186
|
+
Use the URL generated in Step 4 (OAuth2 URL Generator), or construct it manually:
|
|
148
187
|
|
|
149
188
|
```
|
|
150
|
-
https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=
|
|
189
|
+
https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=218900185540&scope=bot%20applications.commands
|
|
151
190
|
```
|
|
152
191
|
|
|
153
192
|
1. Replace `YOUR_CLIENT_ID` with your Application ID
|
|
154
193
|
2. Open the URL in your browser
|
|
155
194
|
3. Select your server and authorize
|
|
156
195
|
|
|
157
|
-
<!-- SCREENSHOT: Bot authorization page -->
|
|
158
|
-
<!--  -->
|
|
159
|
-
*Screenshot placeholder: Discord OAuth2 authorization page*
|
|
160
196
|
|
|
161
197
|
---
|
|
162
198
|
|
|
@@ -242,6 +278,43 @@ Start isolated work on a new branch with its own worktree.
|
|
|
242
278
|
|
|
243
279
|
This is perfect for working on multiple features simultaneously without branch switching.
|
|
244
280
|
|
|
281
|
+
### `/code` β Toggle Passthrough Mode
|
|
282
|
+
|
|
283
|
+
Enable passthrough mode in a thread to send messages directly to OpenCode without slash commands.
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
/code
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**How it works:**
|
|
290
|
+
1. Run `/code` in any thread to enable passthrough mode
|
|
291
|
+
2. Type messages naturally β they're sent directly to OpenCode
|
|
292
|
+
3. Run `/code` again to disable
|
|
293
|
+
|
|
294
|
+
**Example:**
|
|
295
|
+
```
|
|
296
|
+
You: /code
|
|
297
|
+
Bot: β
Passthrough mode enabled for this thread.
|
|
298
|
+
Your messages will be sent directly to OpenCode.
|
|
299
|
+
|
|
300
|
+
You: Add a dark mode toggle to settings
|
|
301
|
+
Bot: π Prompt: Add a dark mode toggle to settings
|
|
302
|
+
[streaming response...]
|
|
303
|
+
|
|
304
|
+
You: Now add a keyboard shortcut for it
|
|
305
|
+
Bot: π Prompt: Now add a keyboard shortcut for it
|
|
306
|
+
[streaming response...]
|
|
307
|
+
|
|
308
|
+
You: /code
|
|
309
|
+
Bot: β Passthrough mode disabled.
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Features:**
|
|
313
|
+
- π± **Mobile-friendly** β no more typing slash commands on phone
|
|
314
|
+
- π§΅ **Thread-scoped** β only affects the specific thread, not the whole channel
|
|
315
|
+
- β³ **Busy indicator** β shows β³ reaction if previous task is still running
|
|
316
|
+
- π **Safe** β ignores bot messages (no infinite loops)
|
|
317
|
+
|
|
245
318
|
---
|
|
246
319
|
|
|
247
320
|
## Usage Workflow
|
|
@@ -278,6 +351,8 @@ Perfect for when you're away from your desk:
|
|
|
278
351
|
4. Watch real-time progress
|
|
279
352
|
5. Use the **Interrupt** button if needed
|
|
280
353
|
|
|
354
|
+
**Pro tip:** Enable passthrough mode with `/code` in a thread for an even smoother mobile experience β just type messages directly without slash commands!
|
|
355
|
+
|
|
281
356
|
### Team Collaboration Workflow
|
|
282
357
|
|
|
283
358
|
Share AI coding sessions with your team:
|
|
@@ -413,7 +488,7 @@ The bot maintains persistent sessions. If you encounter issues:
|
|
|
413
488
|
### Run from source
|
|
414
489
|
|
|
415
490
|
```bash
|
|
416
|
-
git clone https://github.com
|
|
491
|
+
git clone https://github.com/RoundTable02/remote-opencode.git
|
|
417
492
|
cd remote-opencode
|
|
418
493
|
npm install
|
|
419
494
|
|
|
@@ -440,13 +515,15 @@ src/
|
|
|
440
515
|
βββ bot.ts # Discord client initialization
|
|
441
516
|
βββ commands/ # Slash command definitions
|
|
442
517
|
β βββ opencode.ts # Main AI interaction command
|
|
518
|
+
β βββ code.ts # Passthrough mode toggle
|
|
443
519
|
β βββ work.ts # Worktree management
|
|
444
520
|
β βββ setpath.ts # Project registration
|
|
445
521
|
β βββ projects.ts # List projects
|
|
446
522
|
β βββ use.ts # Channel binding
|
|
447
523
|
βββ handlers/ # Interaction handlers
|
|
448
524
|
β βββ interactionHandler.ts
|
|
449
|
-
β
|
|
525
|
+
β βββ buttonHandler.ts
|
|
526
|
+
β βββ messageHandler.ts # Passthrough message handling
|
|
450
527
|
βββ services/ # Core business logic
|
|
451
528
|
β βββ serveManager.ts # OpenCode process management
|
|
452
529
|
β βββ sessionManager.ts # Session state management
|
package/dist/src/bot.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Client, GatewayIntentBits, Events } from 'discord.js';
|
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import { getBotConfig } from './services/configStore.js';
|
|
4
4
|
import { handleInteraction } from './handlers/interactionHandler.js';
|
|
5
|
+
import { handleMessageCreate } from './handlers/messageHandler.js';
|
|
5
6
|
import * as serveManager from './services/serveManager.js';
|
|
6
7
|
export async function startBot() {
|
|
7
8
|
const config = getBotConfig();
|
|
@@ -9,12 +10,17 @@ export async function startBot() {
|
|
|
9
10
|
throw new Error('Bot configuration not found. Run setup first.');
|
|
10
11
|
}
|
|
11
12
|
const client = new Client({
|
|
12
|
-
intents: [
|
|
13
|
+
intents: [
|
|
14
|
+
GatewayIntentBits.Guilds,
|
|
15
|
+
GatewayIntentBits.GuildMessages,
|
|
16
|
+
GatewayIntentBits.MessageContent
|
|
17
|
+
]
|
|
13
18
|
});
|
|
14
19
|
client.once(Events.ClientReady, (c) => {
|
|
15
20
|
console.log(pc.green(`Ready! Logged in as ${pc.bold(c.user.tag)}`));
|
|
16
21
|
});
|
|
17
22
|
client.on(Events.InteractionCreate, handleInteraction);
|
|
23
|
+
client.on(Events.MessageCreate, handleMessageCreate);
|
|
18
24
|
function gracefulShutdown(signal) {
|
|
19
25
|
console.log(pc.yellow(`\n${signal} received. Shutting down gracefully...`));
|
|
20
26
|
serveManager.stopAll();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
|
|
2
|
+
import * as dataStore from '../services/dataStore.js';
|
|
3
|
+
export const code = {
|
|
4
|
+
data: new SlashCommandBuilder()
|
|
5
|
+
.setName('code')
|
|
6
|
+
.setDescription('Toggle passthrough mode - send messages directly to OpenCode'),
|
|
7
|
+
async execute(interaction) {
|
|
8
|
+
const channel = interaction.channel;
|
|
9
|
+
if (!channel?.isThread()) {
|
|
10
|
+
await interaction.reply({
|
|
11
|
+
content: 'β This command only works inside threads.',
|
|
12
|
+
flags: MessageFlags.Ephemeral
|
|
13
|
+
});
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const threadId = channel.id;
|
|
17
|
+
const parentChannelId = channel.parentId;
|
|
18
|
+
if (!parentChannelId) {
|
|
19
|
+
await interaction.reply({
|
|
20
|
+
content: 'β Cannot determine parent channel.',
|
|
21
|
+
flags: MessageFlags.Ephemeral
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const projectPath = dataStore.getChannelProjectPath(parentChannelId);
|
|
26
|
+
if (!projectPath) {
|
|
27
|
+
await interaction.reply({
|
|
28
|
+
content: 'β No project set for this channel. Use `/use <alias>` in the parent channel first.',
|
|
29
|
+
flags: MessageFlags.Ephemeral
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const currentState = dataStore.isPassthroughEnabled(threadId);
|
|
34
|
+
const newState = !currentState;
|
|
35
|
+
dataStore.setPassthroughMode(threadId, newState, interaction.user.id);
|
|
36
|
+
if (newState) {
|
|
37
|
+
await interaction.reply({
|
|
38
|
+
content: 'β
**Passthrough mode enabled** for this thread.\nYour messages will be sent directly to OpenCode.',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
await interaction.reply({
|
|
43
|
+
content: 'β **Passthrough mode disabled.**\nUse `/opencode prompt:"..."` to send commands.',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
@@ -4,9 +4,11 @@ import { projects } from './projects.js';
|
|
|
4
4
|
import { use } from './use.js';
|
|
5
5
|
import { opencode } from './opencode.js';
|
|
6
6
|
import { work } from './work.js';
|
|
7
|
+
import { code } from './code.js';
|
|
7
8
|
export const commands = new Collection();
|
|
8
9
|
commands.set(setpath.data.name, setpath);
|
|
9
10
|
commands.set(projects.data.name, projects);
|
|
10
11
|
commands.set(use.data.name, use);
|
|
11
12
|
commands.set(opencode.data.name, opencode);
|
|
12
13
|
commands.set(work.data.name, work);
|
|
14
|
+
commands.set(code.data.name, code);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
|
|
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 { SSEClient } from '../services/sseClient.js';
|
|
6
|
+
import { formatOutput } from '../utils/messageFormatter.js';
|
|
7
|
+
export async function handleMessageCreate(message) {
|
|
8
|
+
if (message.author.bot)
|
|
9
|
+
return;
|
|
10
|
+
if (message.system)
|
|
11
|
+
return;
|
|
12
|
+
const channel = message.channel;
|
|
13
|
+
if (!channel.isThread())
|
|
14
|
+
return;
|
|
15
|
+
const threadId = channel.id;
|
|
16
|
+
if (!dataStore.isPassthroughEnabled(threadId))
|
|
17
|
+
return;
|
|
18
|
+
const parentChannelId = channel.parentId;
|
|
19
|
+
if (!parentChannelId)
|
|
20
|
+
return;
|
|
21
|
+
const projectPath = dataStore.getChannelProjectPath(parentChannelId);
|
|
22
|
+
if (!projectPath) {
|
|
23
|
+
await message.reply('β No project bound to parent channel. Disable passthrough and use `/use` first.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const worktreeMapping = dataStore.getWorktreeMapping(threadId);
|
|
27
|
+
const effectivePath = worktreeMapping?.worktreePath ?? projectPath;
|
|
28
|
+
const existingClient = sessionManager.getSseClient(threadId);
|
|
29
|
+
if (existingClient && existingClient.isConnected()) {
|
|
30
|
+
await message.react('β³');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const prompt = message.content.trim();
|
|
34
|
+
if (!prompt)
|
|
35
|
+
return;
|
|
36
|
+
const buttons = new ActionRowBuilder()
|
|
37
|
+
.addComponents(new ButtonBuilder()
|
|
38
|
+
.setCustomId(`interrupt_${threadId}`)
|
|
39
|
+
.setLabel('βΈοΈ Interrupt')
|
|
40
|
+
.setStyle(ButtonStyle.Secondary));
|
|
41
|
+
let streamMessage;
|
|
42
|
+
try {
|
|
43
|
+
streamMessage = await channel.send({
|
|
44
|
+
content: `π **Prompt**: ${prompt}\n\nπ Starting OpenCode server...`,
|
|
45
|
+
components: [buttons]
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
let port;
|
|
52
|
+
let sessionId;
|
|
53
|
+
let updateInterval = null;
|
|
54
|
+
let accumulatedText = '';
|
|
55
|
+
let lastContent = '';
|
|
56
|
+
let tick = 0;
|
|
57
|
+
const spinner = ['β ', 'β ', 'β Ή', 'β Έ', 'β Ό', 'β ΄', 'β ¦', 'β §', 'β ', 'β '];
|
|
58
|
+
const updateStreamMessage = async (content, components) => {
|
|
59
|
+
try {
|
|
60
|
+
await streamMessage.edit({ content, components });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
port = await serveManager.spawnServe(effectivePath);
|
|
67
|
+
await updateStreamMessage(`π **Prompt**: ${prompt}\n\nβ³ Waiting for OpenCode server...`, [buttons]);
|
|
68
|
+
await serveManager.waitForReady(port);
|
|
69
|
+
const existingSession = sessionManager.getSessionForThread(threadId);
|
|
70
|
+
if (existingSession && existingSession.projectPath === effectivePath) {
|
|
71
|
+
const isValid = await sessionManager.validateSession(port, existingSession.sessionId);
|
|
72
|
+
if (isValid) {
|
|
73
|
+
sessionId = existingSession.sessionId;
|
|
74
|
+
sessionManager.updateSessionLastUsed(threadId);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
sessionId = await sessionManager.createSession(port);
|
|
78
|
+
sessionManager.setSessionForThread(threadId, sessionId, effectivePath, port);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
sessionId = await sessionManager.createSession(port);
|
|
83
|
+
sessionManager.setSessionForThread(threadId, sessionId, effectivePath, port);
|
|
84
|
+
}
|
|
85
|
+
const sseClient = new SSEClient();
|
|
86
|
+
sseClient.connect(`http://localhost:${port}`);
|
|
87
|
+
sessionManager.setSseClient(threadId, sseClient);
|
|
88
|
+
sseClient.onPartUpdated((part) => {
|
|
89
|
+
accumulatedText = part.text;
|
|
90
|
+
});
|
|
91
|
+
sseClient.onSessionIdle(() => {
|
|
92
|
+
if (updateInterval) {
|
|
93
|
+
clearInterval(updateInterval);
|
|
94
|
+
updateInterval = null;
|
|
95
|
+
}
|
|
96
|
+
(async () => {
|
|
97
|
+
try {
|
|
98
|
+
const formatted = formatOutput(accumulatedText);
|
|
99
|
+
const disabledButtons = new ActionRowBuilder()
|
|
100
|
+
.addComponents(new ButtonBuilder()
|
|
101
|
+
.setCustomId(`interrupt_${threadId}`)
|
|
102
|
+
.setLabel('βΈοΈ Interrupt')
|
|
103
|
+
.setStyle(ButtonStyle.Secondary)
|
|
104
|
+
.setDisabled(true));
|
|
105
|
+
await updateStreamMessage(`π **Prompt**: ${prompt}\n\n\`\`\`\n${formatted}\n\`\`\``, [disabledButtons]);
|
|
106
|
+
await channel.send({ content: 'β
Done' });
|
|
107
|
+
sseClient.disconnect();
|
|
108
|
+
sessionManager.clearSseClient(threadId);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
});
|
|
114
|
+
sseClient.onError((error) => {
|
|
115
|
+
if (updateInterval) {
|
|
116
|
+
clearInterval(updateInterval);
|
|
117
|
+
updateInterval = null;
|
|
118
|
+
}
|
|
119
|
+
(async () => {
|
|
120
|
+
try {
|
|
121
|
+
await updateStreamMessage(`π **Prompt**: ${prompt}\n\nβ Connection error: ${error.message}`, []);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
}
|
|
125
|
+
})();
|
|
126
|
+
});
|
|
127
|
+
updateInterval = setInterval(async () => {
|
|
128
|
+
tick++;
|
|
129
|
+
try {
|
|
130
|
+
const formatted = formatOutput(accumulatedText);
|
|
131
|
+
const spinnerChar = spinner[tick % spinner.length];
|
|
132
|
+
const newContent = formatted || 'Processing...';
|
|
133
|
+
if (newContent !== lastContent || tick % 2 === 0) {
|
|
134
|
+
lastContent = newContent;
|
|
135
|
+
await updateStreamMessage(`π **Prompt**: ${prompt}\n\n${spinnerChar} **Running...**\n\`\`\`\n${newContent}\n\`\`\``, [buttons]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
}
|
|
140
|
+
}, 1000);
|
|
141
|
+
await updateStreamMessage(`π **Prompt**: ${prompt}\n\nπ Sending prompt...`, [buttons]);
|
|
142
|
+
await sessionManager.sendPrompt(port, sessionId, prompt);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (updateInterval) {
|
|
146
|
+
clearInterval(updateInterval);
|
|
147
|
+
}
|
|
148
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
149
|
+
await updateStreamMessage(`π **Prompt**: ${prompt}\n\nβ OpenCode execution failed: ${errorMessage}`, []);
|
|
150
|
+
const client = sessionManager.getSseClient(threadId);
|
|
151
|
+
if (client) {
|
|
152
|
+
client.disconnect();
|
|
153
|
+
sessionManager.clearSseClient(threadId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -150,3 +150,46 @@ export function getWorktreeMappingsByProject(projectPath) {
|
|
|
150
150
|
const data = loadData();
|
|
151
151
|
return data.worktreeMappings?.filter(m => m.projectPath === projectPath) ?? [];
|
|
152
152
|
}
|
|
153
|
+
export function setPassthroughMode(threadId, enabled, userId) {
|
|
154
|
+
const data = loadData();
|
|
155
|
+
if (!data.passthroughThreads) {
|
|
156
|
+
data.passthroughThreads = [];
|
|
157
|
+
}
|
|
158
|
+
const existing = data.passthroughThreads.findIndex(p => p.threadId === threadId);
|
|
159
|
+
if (existing >= 0) {
|
|
160
|
+
data.passthroughThreads[existing] = {
|
|
161
|
+
threadId,
|
|
162
|
+
enabled,
|
|
163
|
+
enabledBy: userId,
|
|
164
|
+
enabledAt: Date.now()
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
data.passthroughThreads.push({
|
|
169
|
+
threadId,
|
|
170
|
+
enabled,
|
|
171
|
+
enabledBy: userId,
|
|
172
|
+
enabledAt: Date.now()
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
saveData(data);
|
|
176
|
+
}
|
|
177
|
+
export function getPassthroughMode(threadId) {
|
|
178
|
+
const data = loadData();
|
|
179
|
+
return data.passthroughThreads?.find(p => p.threadId === threadId);
|
|
180
|
+
}
|
|
181
|
+
export function isPassthroughEnabled(threadId) {
|
|
182
|
+
const passthrough = getPassthroughMode(threadId);
|
|
183
|
+
return passthrough?.enabled ?? false;
|
|
184
|
+
}
|
|
185
|
+
export function removePassthroughMode(threadId) {
|
|
186
|
+
const data = loadData();
|
|
187
|
+
if (!data.passthroughThreads)
|
|
188
|
+
return false;
|
|
189
|
+
const idx = data.passthroughThreads.findIndex(p => p.threadId === threadId);
|
|
190
|
+
if (idx < 0)
|
|
191
|
+
return false;
|
|
192
|
+
data.passthroughThreads.splice(idx, 1);
|
|
193
|
+
saveData(data);
|
|
194
|
+
return true;
|
|
195
|
+
}
|