remote-opencode 1.0.2 β 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/README.md +53 -40
- 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/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
|
|
@@ -75,7 +79,7 @@ npx remote-opencode
|
|
|
75
79
|
### Install from source
|
|
76
80
|
|
|
77
81
|
```bash
|
|
78
|
-
git clone https://github.com
|
|
82
|
+
git clone https://github.com/RoundTable02/remote-opencode.git
|
|
79
83
|
cd remote-opencode
|
|
80
84
|
npm install
|
|
81
85
|
npm run build
|
|
@@ -109,9 +113,7 @@ Before using remote-opencode, you need to create a Discord Application and Bot.
|
|
|
109
113
|
3. Enter a name (e.g., "Remote OpenCode")
|
|
110
114
|
4. Copy the **Application ID** β you'll need this later
|
|
111
115
|
|
|
112
|
-
|
|
113
|
-
<!--  -->
|
|
114
|
-
*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" />
|
|
115
117
|
|
|
116
118
|
### Step 2: Create Bot & Get Token
|
|
117
119
|
|
|
@@ -120,9 +122,6 @@ Before using remote-opencode, you need to create a Discord Application and Bot.
|
|
|
120
122
|
3. **Copy the token immediately** β it's only shown once!
|
|
121
123
|
4. Keep this token secret β never share it publicly
|
|
122
124
|
|
|
123
|
-
<!-- SCREENSHOT: Bot section showing Reset Token button -->
|
|
124
|
-
<!--  -->
|
|
125
|
-
*Screenshot placeholder: Bot settings page with token section highlighted*
|
|
126
125
|
|
|
127
126
|
### Step 3: Enable Required Intents
|
|
128
127
|
|
|
@@ -133,9 +132,7 @@ Still in the **"Bot"** section, scroll down to **"Privileged Gateway Intents"**
|
|
|
133
132
|
|
|
134
133
|
Click **"Save Changes"**
|
|
135
134
|
|
|
136
|
-
|
|
137
|
-
<!--  -->
|
|
138
|
-
*Screenshot placeholder: Intents section with required toggles enabled*
|
|
135
|
+
<img width="1500" alt="image" src="https://github.com/user-attachments/assets/d20406ff-26ad-4204-9771-b157c340846a" />
|
|
139
136
|
|
|
140
137
|
### Step 4: Configure Bot Permissions
|
|
141
138
|
|
|
@@ -164,35 +161,15 @@ The bot needs specific permissions to function properly. You can configure permi
|
|
|
164
161
|
|
|
165
162
|
5. Copy the generated URL at the bottom β this is your bot invite link!
|
|
166
163
|
|
|
167
|
-
<!-- SCREENSHOT: OAuth2 URL Generator showing selected permissions -->
|
|
168
|
-
<!--  -->
|
|
169
|
-
*Screenshot placeholder: OAuth2 URL Generator with bot permissions selected*
|
|
170
164
|
|
|
171
165
|
#### Option B: Manual Permission Calculation
|
|
172
166
|
|
|
173
167
|
If you're building the URL manually, use this permission value:
|
|
174
168
|
|
|
175
169
|
```
|
|
176
|
-
https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=
|
|
170
|
+
https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=311385214016&integration_type=0&scope=bot+applications.commands
|
|
177
171
|
```
|
|
178
172
|
|
|
179
|
-
**Permission Breakdown (343932274624):**
|
|
180
|
-
|
|
181
|
-
| Permission | Value | Purpose |
|
|
182
|
-
|------------|-------|---------|
|
|
183
|
-
| View Channels | 1024 | Access channels and read messages |
|
|
184
|
-
| Send Messages | 2048 | Send messages to channels |
|
|
185
|
-
| Create Public Threads | 68719476736 | Create threads for each `/opencode` command |
|
|
186
|
-
| Send Messages in Threads | 274877906944 | Reply within created threads |
|
|
187
|
-
| Embed Links | 16384 | Send rich embed messages with formatting |
|
|
188
|
-
| Read Message History | 65536 | Access previous messages for context |
|
|
189
|
-
| Add Reactions | 64 | Add button components (uses emoji reactions) |
|
|
190
|
-
| Use Slash Commands | 2147483648 | Register and respond to slash commands |
|
|
191
|
-
|
|
192
|
-
**Optional Permissions (not required):**
|
|
193
|
-
- **Attach Files** (32768) β Only if you want to upload files
|
|
194
|
-
- **Mention @everyone** (131072) β Only if bot needs to ping everyone
|
|
195
|
-
|
|
196
173
|
**Important:** The URL must include `applications.commands` scope for slash commands to work!
|
|
197
174
|
|
|
198
175
|
### Step 5: Get Your Server (Guild) ID
|
|
@@ -202,25 +179,20 @@ https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=34
|
|
|
202
179
|
3. Right-click on your server name in the sidebar
|
|
203
180
|
4. Click **"Copy Server ID"**
|
|
204
181
|
|
|
205
|
-
|
|
206
|
-
<!--  -->
|
|
207
|
-
*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" />
|
|
208
183
|
|
|
209
184
|
### Step 6: Invite Bot to Your Server
|
|
210
185
|
|
|
211
186
|
Use the URL generated in Step 4 (OAuth2 URL Generator), or construct it manually:
|
|
212
187
|
|
|
213
188
|
```
|
|
214
|
-
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
|
|
215
190
|
```
|
|
216
191
|
|
|
217
192
|
1. Replace `YOUR_CLIENT_ID` with your Application ID
|
|
218
193
|
2. Open the URL in your browser
|
|
219
194
|
3. Select your server and authorize
|
|
220
195
|
|
|
221
|
-
<!-- SCREENSHOT: Bot authorization page -->
|
|
222
|
-
<!--  -->
|
|
223
|
-
*Screenshot placeholder: Discord OAuth2 authorization page*
|
|
224
196
|
|
|
225
197
|
---
|
|
226
198
|
|
|
@@ -306,6 +278,43 @@ Start isolated work on a new branch with its own worktree.
|
|
|
306
278
|
|
|
307
279
|
This is perfect for working on multiple features simultaneously without branch switching.
|
|
308
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
|
+
|
|
309
318
|
---
|
|
310
319
|
|
|
311
320
|
## Usage Workflow
|
|
@@ -342,6 +351,8 @@ Perfect for when you're away from your desk:
|
|
|
342
351
|
4. Watch real-time progress
|
|
343
352
|
5. Use the **Interrupt** button if needed
|
|
344
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
|
+
|
|
345
356
|
### Team Collaboration Workflow
|
|
346
357
|
|
|
347
358
|
Share AI coding sessions with your team:
|
|
@@ -477,7 +488,7 @@ The bot maintains persistent sessions. If you encounter issues:
|
|
|
477
488
|
### Run from source
|
|
478
489
|
|
|
479
490
|
```bash
|
|
480
|
-
git clone https://github.com
|
|
491
|
+
git clone https://github.com/RoundTable02/remote-opencode.git
|
|
481
492
|
cd remote-opencode
|
|
482
493
|
npm install
|
|
483
494
|
|
|
@@ -504,13 +515,15 @@ src/
|
|
|
504
515
|
βββ bot.ts # Discord client initialization
|
|
505
516
|
βββ commands/ # Slash command definitions
|
|
506
517
|
β βββ opencode.ts # Main AI interaction command
|
|
518
|
+
β βββ code.ts # Passthrough mode toggle
|
|
507
519
|
β βββ work.ts # Worktree management
|
|
508
520
|
β βββ setpath.ts # Project registration
|
|
509
521
|
β βββ projects.ts # List projects
|
|
510
522
|
β βββ use.ts # Channel binding
|
|
511
523
|
βββ handlers/ # Interaction handlers
|
|
512
524
|
β βββ interactionHandler.ts
|
|
513
|
-
β
|
|
525
|
+
β βββ buttonHandler.ts
|
|
526
|
+
β βββ messageHandler.ts # Passthrough message handling
|
|
514
527
|
βββ services/ # Core business logic
|
|
515
528
|
β βββ serveManager.ts # OpenCode process management
|
|
516
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
|
+
}
|