@yu_robotics/remote-cli 1.1.21 → 1.1.33
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/client/MessageHandler.d.ts +23 -55
- package/dist/client/MessageHandler.d.ts.map +1 -1
- package/dist/client/MessageHandler.js +289 -265
- package/dist/client/MessageHandler.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +22 -11
- package/dist/commands/start.js.map +1 -1
- package/dist/executor/ClaudePersistentExecutor.d.ts +1 -1
- package/dist/executor/ClaudePersistentExecutor.d.ts.map +1 -1
- package/dist/executor/ClaudePersistentExecutor.js +17 -6
- package/dist/executor/ClaudePersistentExecutor.js.map +1 -1
- package/dist/executor/GeminiExecutor.d.ts +12 -0
- package/dist/executor/GeminiExecutor.d.ts.map +1 -1
- package/dist/executor/GeminiExecutor.js +174 -54
- package/dist/executor/GeminiExecutor.js.map +1 -1
- package/dist/executor/acp/SessionManager.d.ts +8 -0
- package/dist/executor/acp/SessionManager.d.ts.map +1 -1
- package/dist/executor/acp/SessionManager.js +18 -0
- package/dist/executor/acp/SessionManager.js.map +1 -1
- package/dist/executor/index.d.ts +1 -1
- package/dist/executor/index.d.ts.map +1 -1
- package/dist/executor/index.js +3 -3
- package/dist/executor/index.js.map +1 -1
- package/dist/thread/ThreadExecutorPool.d.ts +57 -0
- package/dist/thread/ThreadExecutorPool.d.ts.map +1 -0
- package/dist/thread/ThreadExecutorPool.js +104 -0
- package/dist/thread/ThreadExecutorPool.js.map +1 -0
- package/dist/thread/ThreadManager.d.ts +65 -0
- package/dist/thread/ThreadManager.d.ts.map +1 -0
- package/dist/thread/ThreadManager.js +182 -0
- package/dist/thread/ThreadManager.js.map +1 -0
- package/dist/thread/index.d.ts +6 -0
- package/dist/thread/index.d.ts.map +1 -0
- package/dist/thread/index.js +12 -0
- package/dist/thread/index.js.map +1 -0
- package/dist/thread/types.d.ts +28 -0
- package/dist/thread/types.d.ts.map +1 -0
- package/dist/thread/types.js +11 -0
- package/dist/thread/types.js.map +1 -0
- package/dist/types/index.d.ts +12 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,50 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MessageHandler = void 0;
|
|
4
|
-
const executor_1 = require("../executor");
|
|
5
4
|
const hooks_1 = require("../hooks");
|
|
6
5
|
const FileReadDetector_1 = require("../utils/FileReadDetector");
|
|
7
6
|
const child_process_1 = require("child_process");
|
|
8
7
|
/**
|
|
9
|
-
* Message Handler
|
|
10
|
-
*
|
|
8
|
+
* Thread-aware Message Handler.
|
|
9
|
+
* Each thread has its own executor process managed by ThreadExecutorPool.
|
|
10
|
+
* Commands without a threadId are routed to the default thread.
|
|
11
11
|
*/
|
|
12
12
|
class MessageHandler {
|
|
13
13
|
wsClient;
|
|
14
|
-
|
|
14
|
+
threadPool;
|
|
15
|
+
threadManager;
|
|
15
16
|
directoryGuard;
|
|
16
17
|
config;
|
|
17
18
|
isDestroyed = false;
|
|
18
|
-
isExecuting = false;
|
|
19
19
|
currentOpenId;
|
|
20
20
|
notificationAdapter;
|
|
21
|
-
constructor(wsClient,
|
|
21
|
+
constructor(wsClient, threadPool, threadManager, directoryGuard, config) {
|
|
22
22
|
this.wsClient = wsClient;
|
|
23
|
-
this.
|
|
23
|
+
this.threadPool = threadPool;
|
|
24
|
+
this.threadManager = threadManager;
|
|
24
25
|
this.directoryGuard = directoryGuard;
|
|
25
26
|
this.config = config;
|
|
26
|
-
// Initialize Feishu notification adapter
|
|
27
27
|
this.notificationAdapter = new hooks_1.FeishuNotificationAdapter(wsClient);
|
|
28
28
|
this.notificationAdapter.register();
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* Handle message (supports new IncomingMessage format)
|
|
32
|
-
* @param message Message object
|
|
33
32
|
*/
|
|
34
33
|
async handleMessage(message) {
|
|
35
|
-
|
|
36
|
-
if (this.isDestroyed) {
|
|
34
|
+
if (this.isDestroyed)
|
|
37
35
|
return;
|
|
38
|
-
}
|
|
39
|
-
// Validate message structure
|
|
40
36
|
if (!message || !this.isValidMessage(message)) {
|
|
41
|
-
this.sendResponse(message?.messageId || 'unknown', {
|
|
37
|
+
this.sendResponse(message?.messageId || 'unknown', undefined, {
|
|
42
38
|
success: false,
|
|
43
39
|
error: 'Invalid message format',
|
|
44
40
|
});
|
|
45
41
|
return;
|
|
46
42
|
}
|
|
47
|
-
// Handle different types of messages
|
|
48
43
|
switch (message.type) {
|
|
49
44
|
case 'status':
|
|
50
45
|
await this.handleStatusQuery(message.messageId);
|
|
@@ -53,53 +48,56 @@ class MessageHandler {
|
|
|
53
48
|
await this.handleCommandMessage(message);
|
|
54
49
|
return;
|
|
55
50
|
case 'heartbeat':
|
|
56
|
-
// Silently ignore heartbeat responses from server
|
|
57
51
|
return;
|
|
58
52
|
case 'binding_confirm':
|
|
59
|
-
// Silently ignore binding confirmation from server
|
|
60
53
|
return;
|
|
61
54
|
default:
|
|
62
|
-
this.sendResponse(message.messageId, {
|
|
55
|
+
this.sendResponse(message.messageId, undefined, {
|
|
63
56
|
success: false,
|
|
64
57
|
error: `Unknown message type: ${message.type}`,
|
|
65
58
|
});
|
|
66
59
|
}
|
|
67
60
|
}
|
|
68
61
|
/**
|
|
69
|
-
* Handle command message
|
|
62
|
+
* Handle command message — route to the correct thread executor.
|
|
70
63
|
*/
|
|
71
64
|
async handleCommandMessage(message) {
|
|
72
|
-
const { messageId, content, workingDirectory, openId, isSlashCommand } = message;
|
|
73
|
-
// Store openId for response routing and notifications
|
|
65
|
+
const { messageId, content, workingDirectory, openId, isSlashCommand, threadId } = message;
|
|
74
66
|
this.currentOpenId = openId;
|
|
75
67
|
this.notificationAdapter.setCurrentOpenId(openId);
|
|
76
|
-
//
|
|
68
|
+
// Resolve target thread — fall back to default if not specified
|
|
69
|
+
const thread = threadId
|
|
70
|
+
? this.threadManager.getThread(threadId)
|
|
71
|
+
: this.threadManager.getDefaultThread();
|
|
72
|
+
if (!thread) {
|
|
73
|
+
this.sendResponse(messageId, undefined, {
|
|
74
|
+
success: false,
|
|
75
|
+
error: `Thread not found: ${threadId}`,
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const resolvedThreadId = thread.id;
|
|
80
|
+
const executor = this.threadPool.getExecutor(resolvedThreadId);
|
|
81
|
+
// Handle /abort for this specific thread (bypasses busy check)
|
|
77
82
|
if (content?.trim() === '/abort') {
|
|
78
|
-
await this.handleAbortCommand(messageId);
|
|
83
|
+
await this.handleAbortCommand(messageId, resolvedThreadId, executor);
|
|
79
84
|
return;
|
|
80
85
|
}
|
|
81
86
|
// Check if executor is waiting for interactive input
|
|
82
|
-
if ('isWaitingInput' in
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
87
|
+
if ('isWaitingInput' in executor && typeof executor.isWaitingInput === 'function') {
|
|
88
|
+
const ex = executor;
|
|
89
|
+
if (ex.isWaitingInput()) {
|
|
85
90
|
const input = content?.trim();
|
|
86
91
|
if (input) {
|
|
87
|
-
const sent =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
this.sendResponse(messageId, {
|
|
96
|
-
success: false,
|
|
97
|
-
error: '❌ Failed to send input - executor is no longer waiting',
|
|
98
|
-
});
|
|
99
|
-
}
|
|
92
|
+
const sent = ex.sendInput(input);
|
|
93
|
+
this.sendResponse(messageId, resolvedThreadId, {
|
|
94
|
+
success: sent,
|
|
95
|
+
output: sent ? `✅ Sent: "${input}"` : undefined,
|
|
96
|
+
error: sent ? undefined : '❌ Failed to send input - executor is no longer waiting',
|
|
97
|
+
});
|
|
100
98
|
}
|
|
101
99
|
else {
|
|
102
|
-
this.sendResponse(messageId, {
|
|
100
|
+
this.sendResponse(messageId, resolvedThreadId, {
|
|
103
101
|
success: false,
|
|
104
102
|
error: '❌ Please provide a non-empty input',
|
|
105
103
|
});
|
|
@@ -107,19 +105,18 @@ class MessageHandler {
|
|
|
107
105
|
return;
|
|
108
106
|
}
|
|
109
107
|
}
|
|
110
|
-
//
|
|
111
|
-
if (this.
|
|
112
|
-
this.sendResponse(messageId, {
|
|
108
|
+
// Per-thread busy check
|
|
109
|
+
if (this.threadPool.isThreadBusy(resolvedThreadId)) {
|
|
110
|
+
this.sendResponse(messageId, resolvedThreadId, {
|
|
113
111
|
success: false,
|
|
114
|
-
error:
|
|
112
|
+
error: `Thread "${thread.name}" is busy. Send /abort to cancel the running task, or use another thread.`,
|
|
115
113
|
});
|
|
116
114
|
return;
|
|
117
115
|
}
|
|
118
|
-
//
|
|
116
|
+
// Validate and set working directory if provided
|
|
119
117
|
if (workingDirectory) {
|
|
120
|
-
// Verify directory is in the whitelist
|
|
121
118
|
if (!this.directoryGuard.isSafePath(workingDirectory)) {
|
|
122
|
-
this.sendResponse(messageId, {
|
|
119
|
+
this.sendResponse(messageId, resolvedThreadId, {
|
|
123
120
|
success: false,
|
|
124
121
|
error: `Directory not in whitelist: ${workingDirectory}\n\nAllowed directories:\n${this.directoryGuard
|
|
125
122
|
.getAllowedDirectories()
|
|
@@ -128,49 +125,44 @@ class MessageHandler {
|
|
|
128
125
|
});
|
|
129
126
|
return;
|
|
130
127
|
}
|
|
131
|
-
|
|
132
|
-
await this.executor.setWorkingDirectory(workingDirectory);
|
|
128
|
+
await executor.setWorkingDirectory(workingDirectory);
|
|
133
129
|
}
|
|
134
130
|
try {
|
|
135
|
-
this.
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
this.threadPool.setThreadBusy(resolvedThreadId, true);
|
|
132
|
+
// Update thread activity timestamp
|
|
133
|
+
await this.threadManager.updateThread(resolvedThreadId, { lastActiveAt: Date.now() });
|
|
134
|
+
const builtInResult = await this.handleBuiltInCommand(messageId, resolvedThreadId, content, executor);
|
|
135
|
+
if (builtInResult)
|
|
139
136
|
return;
|
|
140
|
-
}
|
|
141
|
-
// Check if this is a passthrough slash command from server
|
|
142
137
|
if (isSlashCommand) {
|
|
143
138
|
console.log(`[MessageHandler] Executing passthrough slash command: ${content}`);
|
|
144
|
-
await this.executeSlashCommand(messageId, content);
|
|
139
|
+
await this.executeSlashCommand(messageId, resolvedThreadId, content, executor);
|
|
145
140
|
return;
|
|
146
141
|
}
|
|
147
|
-
// Expand command shortcuts
|
|
148
142
|
const expandedContent = this.expandCommandShortcuts(content);
|
|
149
|
-
// Detect file-reading intent and inject hint for mobile optimization
|
|
150
143
|
const processedContent = (0, FileReadDetector_1.processFileReadContent)(expandedContent);
|
|
151
|
-
|
|
152
|
-
await this.executeCommand(messageId, processedContent);
|
|
144
|
+
await this.executeCommand(messageId, resolvedThreadId, processedContent, executor);
|
|
153
145
|
}
|
|
154
146
|
catch (error) {
|
|
155
|
-
this.
|
|
147
|
+
this.threadPool.setThreadError(resolvedThreadId, true);
|
|
148
|
+
this.sendResponse(messageId, resolvedThreadId, {
|
|
156
149
|
success: false,
|
|
157
150
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
158
151
|
});
|
|
159
152
|
}
|
|
160
153
|
finally {
|
|
161
|
-
this.
|
|
154
|
+
this.threadPool.setThreadBusy(resolvedThreadId, false);
|
|
162
155
|
}
|
|
163
156
|
}
|
|
164
157
|
/**
|
|
165
|
-
* Handle abort command
|
|
166
|
-
* Can be executed even when executor is busy
|
|
158
|
+
* Handle /abort command for a specific thread executor
|
|
167
159
|
*/
|
|
168
|
-
async handleAbortCommand(messageId) {
|
|
169
|
-
const wasExecuting = this.
|
|
170
|
-
const aborted = await
|
|
160
|
+
async handleAbortCommand(messageId, threadId, executor) {
|
|
161
|
+
const wasExecuting = this.threadPool.isThreadBusy(threadId);
|
|
162
|
+
const aborted = await executor.abort();
|
|
171
163
|
if (aborted) {
|
|
172
|
-
this.
|
|
173
|
-
this.sendResponse(messageId, {
|
|
164
|
+
this.threadPool.setThreadBusy(threadId, false);
|
|
165
|
+
this.sendResponse(messageId, threadId, {
|
|
174
166
|
success: true,
|
|
175
167
|
output: wasExecuting
|
|
176
168
|
? '✅ Current command has been aborted'
|
|
@@ -178,7 +170,7 @@ class MessageHandler {
|
|
|
178
170
|
});
|
|
179
171
|
}
|
|
180
172
|
else {
|
|
181
|
-
this.sendResponse(messageId, {
|
|
173
|
+
this.sendResponse(messageId, threadId, {
|
|
182
174
|
success: true,
|
|
183
175
|
output: 'ℹ️ No command is currently executing',
|
|
184
176
|
});
|
|
@@ -188,207 +180,280 @@ class MessageHandler {
|
|
|
188
180
|
* Handle status query
|
|
189
181
|
*/
|
|
190
182
|
async handleStatusQuery(messageId) {
|
|
183
|
+
const defaultThread = this.threadManager.getDefaultThread();
|
|
184
|
+
const defaultExecutor = this.threadPool.getExecutor(defaultThread.id);
|
|
191
185
|
this.wsClient.send({
|
|
192
186
|
type: 'status',
|
|
193
187
|
messageId,
|
|
194
188
|
status: {
|
|
195
189
|
connected: this.wsClient.isConnected(),
|
|
196
190
|
allowedDirectories: this.directoryGuard.getAllowedDirectories(),
|
|
197
|
-
currentWorkingDirectory:
|
|
191
|
+
currentWorkingDirectory: defaultExecutor.getCurrentWorkingDirectory(),
|
|
192
|
+
threads: this.threadPool.getSummaries(),
|
|
198
193
|
},
|
|
199
194
|
timestamp: Date.now(),
|
|
200
195
|
});
|
|
201
196
|
}
|
|
202
|
-
/**
|
|
203
|
-
* Validate message structure
|
|
204
|
-
*/
|
|
205
197
|
isValidMessage(message) {
|
|
206
|
-
if (!message || typeof message !== 'object')
|
|
198
|
+
if (!message || typeof message !== 'object')
|
|
207
199
|
return false;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return true; // Non-command messages don't need further validation
|
|
211
|
-
}
|
|
200
|
+
if (message.type !== 'command')
|
|
201
|
+
return true;
|
|
212
202
|
return Boolean(message.messageId && message.content);
|
|
213
203
|
}
|
|
214
204
|
/**
|
|
215
|
-
* Handle built-in commands
|
|
216
|
-
*
|
|
205
|
+
* Handle built-in commands (thread-scoped).
|
|
206
|
+
* Returns true if handled.
|
|
217
207
|
*/
|
|
218
|
-
async handleBuiltInCommand(messageId, content) {
|
|
208
|
+
async handleBuiltInCommand(messageId, threadId, content, executor) {
|
|
219
209
|
const trimmed = content.trim();
|
|
220
|
-
// /status command
|
|
221
210
|
if (trimmed === '/status') {
|
|
222
|
-
const cwd =
|
|
211
|
+
const cwd = executor.getCurrentWorkingDirectory();
|
|
223
212
|
const allowedDirs = this.directoryGuard.getAllowedDirectories();
|
|
224
|
-
this.
|
|
213
|
+
const threads = this.threadPool.getSummaries();
|
|
214
|
+
const threadList = threads
|
|
215
|
+
.map(t => ` • ${t.name}${t.status === 'running' ? ' 🔄' : t.status === 'error' ? ' ❌' : ' ✅'} (${t.status})`)
|
|
216
|
+
.join('\n');
|
|
217
|
+
this.sendResponse(messageId, threadId, {
|
|
225
218
|
success: true,
|
|
226
219
|
output: `📊 Status:
|
|
227
220
|
- Working Directory: ${cwd}
|
|
228
221
|
- Allowed Directories: ${allowedDirs.join(', ')}
|
|
229
|
-
- Connection: Active
|
|
222
|
+
- Connection: Active
|
|
223
|
+
- Threads:\n${threadList}`,
|
|
230
224
|
});
|
|
231
225
|
return true;
|
|
232
226
|
}
|
|
233
|
-
// /help command
|
|
234
227
|
if (trimmed === '/help') {
|
|
235
|
-
this.sendResponse(messageId, {
|
|
228
|
+
this.sendResponse(messageId, threadId, {
|
|
236
229
|
success: true,
|
|
237
230
|
output: `📖 Available commands:
|
|
238
231
|
- /help - Show this help message
|
|
239
|
-
- /status - Show current status
|
|
240
|
-
- /abort - Abort the currently executing command
|
|
241
|
-
- /clear - Clear conversation context
|
|
232
|
+
- /status - Show current status and threads
|
|
233
|
+
- /abort - Abort the currently executing command in this thread
|
|
234
|
+
- /clear - Clear conversation context for this thread
|
|
242
235
|
- /compact - Compress conversation history to reduce context size
|
|
243
|
-
- /cd <directory> - Change working directory
|
|
236
|
+
- /cd <directory> - Change working directory for this thread
|
|
244
237
|
- /backend - List available AI backends and switch between them
|
|
238
|
+
- /thread list - List all threads with their status
|
|
239
|
+
- /thread new [name] - Create a new thread
|
|
240
|
+
- /thread delete <name> - Delete a thread (only when idle)
|
|
245
241
|
You can also use natural language commands to control Claude Code CLI.`,
|
|
246
242
|
});
|
|
247
243
|
return true;
|
|
248
244
|
}
|
|
249
|
-
// /clear command
|
|
250
245
|
if (trimmed === '/clear') {
|
|
251
|
-
|
|
252
|
-
this.sendResponse(messageId, {
|
|
246
|
+
executor.resetContext();
|
|
247
|
+
this.sendResponse(messageId, threadId, {
|
|
253
248
|
success: true,
|
|
254
249
|
output: '✅ Conversation context cleared',
|
|
255
250
|
});
|
|
256
251
|
return true;
|
|
257
252
|
}
|
|
258
|
-
// /compact command - compress conversation history via Claude CLI's built-in /compact
|
|
259
253
|
if (trimmed === '/compact') {
|
|
260
|
-
if (!('compactWhenFull' in
|
|
261
|
-
this.sendResponse(messageId, {
|
|
254
|
+
if (!('compactWhenFull' in executor && typeof executor.compactWhenFull === 'function')) {
|
|
255
|
+
this.sendResponse(messageId, threadId, {
|
|
262
256
|
success: false,
|
|
263
257
|
error: '/compact is not supported in this executor mode',
|
|
264
258
|
});
|
|
265
259
|
return true;
|
|
266
260
|
}
|
|
267
|
-
this.sendStreamChunk(messageId, '🗜️ Compressing conversation history...\n');
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
this.sendStreamChunk(messageId, chunk);
|
|
261
|
+
this.sendStreamChunk(messageId, threadId, '🗜️ Compressing conversation history...\n');
|
|
262
|
+
const result = await executor.compactWhenFull((chunk) => {
|
|
263
|
+
this.sendStreamChunk(messageId, threadId, chunk);
|
|
271
264
|
});
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
error: result.error || 'Compaction failed',
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
this.sendResponse(messageId, {
|
|
280
|
-
success: true,
|
|
281
|
-
output: '✅ Conversation history compressed',
|
|
282
|
-
});
|
|
283
|
-
}
|
|
265
|
+
this.sendResponse(messageId, threadId, result.success
|
|
266
|
+
? { success: true, output: '✅ Conversation history compressed' }
|
|
267
|
+
: { success: false, error: result.error || 'Compaction failed' });
|
|
284
268
|
return true;
|
|
285
269
|
}
|
|
286
|
-
// /backend command
|
|
287
270
|
if (trimmed === '/backend' || trimmed.startsWith('/backend ')) {
|
|
288
|
-
await this.handleBackendCommand(messageId, trimmed);
|
|
271
|
+
await this.handleBackendCommand(messageId, threadId, trimmed);
|
|
289
272
|
return true;
|
|
290
273
|
}
|
|
291
|
-
// /cd command
|
|
292
274
|
if (trimmed.startsWith('/cd')) {
|
|
293
275
|
const parts = trimmed.split(/\s+/);
|
|
294
276
|
if (parts.length < 2) {
|
|
295
|
-
this.sendResponse(messageId, {
|
|
296
|
-
success: false,
|
|
297
|
-
error: 'Usage: /cd <directory>',
|
|
298
|
-
});
|
|
277
|
+
this.sendResponse(messageId, threadId, { success: false, error: 'Usage: /cd <directory>' });
|
|
299
278
|
return true;
|
|
300
279
|
}
|
|
301
280
|
const targetDir = parts.slice(1).join(' ');
|
|
302
281
|
try {
|
|
303
|
-
await
|
|
304
|
-
const newCwd =
|
|
305
|
-
//
|
|
306
|
-
await this.
|
|
307
|
-
this.sendResponse(messageId, {
|
|
282
|
+
await executor.setWorkingDirectory(targetDir);
|
|
283
|
+
const newCwd = executor.getCurrentWorkingDirectory();
|
|
284
|
+
// Persist the thread's working directory (restored on next startup via ThreadExecutorPool)
|
|
285
|
+
await this.threadManager.updateThread(threadId, { workingDirectory: newCwd });
|
|
286
|
+
this.sendResponse(messageId, threadId, {
|
|
308
287
|
success: true,
|
|
309
288
|
output: `✅ Changed working directory to: ${newCwd}`,
|
|
310
289
|
});
|
|
311
290
|
}
|
|
312
291
|
catch (error) {
|
|
313
|
-
this.sendResponse(messageId, {
|
|
292
|
+
this.sendResponse(messageId, threadId, {
|
|
314
293
|
success: false,
|
|
315
|
-
error: error instanceof Error
|
|
316
|
-
? error.message
|
|
317
|
-
: 'Failed to change directory',
|
|
294
|
+
error: error instanceof Error ? error.message : 'Failed to change directory',
|
|
318
295
|
});
|
|
319
296
|
}
|
|
320
297
|
return true;
|
|
321
298
|
}
|
|
299
|
+
if (trimmed === '/thread' || trimmed.startsWith('/thread ')) {
|
|
300
|
+
await this.handleThreadCommand(messageId, threadId, trimmed);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
322
303
|
return false;
|
|
323
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Handle /thread subcommands
|
|
307
|
+
*/
|
|
308
|
+
async handleThreadCommand(messageId, callerThreadId, trimmed) {
|
|
309
|
+
const parts = trimmed.split(/\s+/);
|
|
310
|
+
const sub = parts[1]; // list | new | delete
|
|
311
|
+
if (!sub || sub === 'list') {
|
|
312
|
+
const summaries = this.threadPool.getSummaries();
|
|
313
|
+
const lines = summaries.map(t => {
|
|
314
|
+
const icon = t.status === 'running' ? '🔄' : t.status === 'error' ? '❌' : '✅';
|
|
315
|
+
const current = t.id === callerThreadId ? ' ← (this thread)' : '';
|
|
316
|
+
return `${icon} ${t.name}${current}`;
|
|
317
|
+
});
|
|
318
|
+
this.sendResponse(messageId, callerThreadId, {
|
|
319
|
+
success: true,
|
|
320
|
+
output: `🧵 Threads:\n${lines.join('\n')}\n\nUse /thread new [name] to create a new thread.\nReply to a thread's card to send commands to it.`,
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (sub === 'new') {
|
|
325
|
+
const name = parts[2];
|
|
326
|
+
const callerThread = this.threadManager.getThread(callerThreadId) || this.threadManager.getDefaultThread();
|
|
327
|
+
const callerExecutor = this.threadPool.getExecutor(callerThread.id);
|
|
328
|
+
const cwd = callerExecutor.getCurrentWorkingDirectory();
|
|
329
|
+
try {
|
|
330
|
+
const newThread = await this.threadManager.createThread(name || this.generateThreadName(), cwd);
|
|
331
|
+
// Use newThread.id so the router maps this card to the new thread,
|
|
332
|
+
// enabling the user to reply to this card to target the new thread.
|
|
333
|
+
this.sendResponse(messageId, newThread.id, {
|
|
334
|
+
success: true,
|
|
335
|
+
output: `✅ Thread "${newThread.name}" created.\nReply to this card to send the first command to the new thread.`,
|
|
336
|
+
threads: this.threadPool.getSummaries(),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
this.sendResponse(messageId, callerThreadId, {
|
|
341
|
+
success: false,
|
|
342
|
+
error: error instanceof Error ? error.message : 'Failed to create thread',
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (sub === 'delete') {
|
|
348
|
+
const name = parts[2];
|
|
349
|
+
if (!name) {
|
|
350
|
+
this.sendResponse(messageId, callerThreadId, {
|
|
351
|
+
success: false,
|
|
352
|
+
error: 'Usage: /thread delete <name>',
|
|
353
|
+
});
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const target = this.threadManager.getThreadByName(name);
|
|
357
|
+
if (!target) {
|
|
358
|
+
this.sendResponse(messageId, callerThreadId, {
|
|
359
|
+
success: false,
|
|
360
|
+
error: `Thread "${name}" not found.`,
|
|
361
|
+
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (this.threadPool.isThreadBusy(target.id)) {
|
|
365
|
+
this.sendResponse(messageId, callerThreadId, {
|
|
366
|
+
success: false,
|
|
367
|
+
error: `Cannot delete thread "${name}" while it is running. Send /abort first.`,
|
|
368
|
+
});
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
await this.threadPool.destroyThread(target.id);
|
|
373
|
+
await this.threadManager.deleteThread(target.id);
|
|
374
|
+
this.sendResponse(messageId, callerThreadId, {
|
|
375
|
+
success: true,
|
|
376
|
+
output: `✅ Thread "${name}" deleted.`,
|
|
377
|
+
threads: this.threadPool.getSummaries(),
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
this.sendResponse(messageId, callerThreadId, {
|
|
382
|
+
success: false,
|
|
383
|
+
error: error instanceof Error ? error.message : 'Failed to delete thread',
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
this.sendResponse(messageId, callerThreadId, {
|
|
389
|
+
success: false,
|
|
390
|
+
error: `Unknown /thread subcommand: ${sub}\nUsage: /thread list | /thread new [name] | /thread delete <name>`,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Generate a unique auto-name for a new thread (e.g. "thread-2")
|
|
395
|
+
*/
|
|
396
|
+
generateThreadName() {
|
|
397
|
+
const existing = new Set(this.threadManager.listThreads().map(t => t.name));
|
|
398
|
+
for (let i = 2; i <= 99; i++) {
|
|
399
|
+
const name = `thread-${i}`;
|
|
400
|
+
if (!existing.has(name))
|
|
401
|
+
return name;
|
|
402
|
+
}
|
|
403
|
+
return `thread-${Date.now()}`;
|
|
404
|
+
}
|
|
324
405
|
/**
|
|
325
406
|
* Expand command shortcuts
|
|
326
407
|
*/
|
|
327
408
|
expandCommandShortcuts(content) {
|
|
328
409
|
const trimmed = content.trim();
|
|
329
|
-
|
|
330
|
-
if (trimmed === '/r' || trimmed === '/resume') {
|
|
410
|
+
if (trimmed === '/r' || trimmed === '/resume')
|
|
331
411
|
return 'Please resume the previous conversation';
|
|
332
|
-
|
|
333
|
-
if (trimmed === '/c' || trimmed === '/continue') {
|
|
412
|
+
if (trimmed === '/c' || trimmed === '/continue')
|
|
334
413
|
return 'Please continue from where we left off';
|
|
335
|
-
}
|
|
336
414
|
return content;
|
|
337
415
|
}
|
|
338
416
|
/**
|
|
339
417
|
* Execute passthrough slash command using local Claude CLI
|
|
340
|
-
* This allows users to use their custom slash commands
|
|
341
418
|
*/
|
|
342
|
-
async executeSlashCommand(messageId, command) {
|
|
419
|
+
async executeSlashCommand(messageId, threadId, command, executor) {
|
|
343
420
|
return new Promise((resolve) => {
|
|
344
421
|
const chunks = [];
|
|
345
422
|
const errorChunks = [];
|
|
346
423
|
console.log(`[MessageHandler] Spawning Claude CLI for command: ${command}`);
|
|
347
|
-
// Spawn Claude CLI with the slash command
|
|
348
|
-
// Use --print to get output and exit
|
|
349
424
|
const child = (0, child_process_1.spawn)('claude', [command, '--print'], {
|
|
350
|
-
cwd:
|
|
425
|
+
cwd: executor.getCurrentWorkingDirectory(),
|
|
351
426
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
352
|
-
env: {
|
|
353
|
-
...process.env,
|
|
354
|
-
CLAUDECODE: '', // Prevent nested session error
|
|
355
|
-
},
|
|
427
|
+
env: { ...process.env, CLAUDECODE: '' },
|
|
356
428
|
});
|
|
357
|
-
// Handle stdout (stream chunks)
|
|
358
429
|
child.stdout?.on('data', (data) => {
|
|
359
430
|
const chunk = data.toString();
|
|
360
431
|
chunks.push(chunk);
|
|
361
|
-
this.sendStreamChunk(messageId, chunk);
|
|
432
|
+
this.sendStreamChunk(messageId, threadId, chunk);
|
|
362
433
|
});
|
|
363
|
-
// Handle stderr
|
|
364
434
|
child.stderr?.on('data', (data) => {
|
|
365
|
-
|
|
366
|
-
errorChunks.push(chunk);
|
|
367
|
-
console.error(`[MessageHandler] Claude stderr: ${chunk}`);
|
|
435
|
+
errorChunks.push(data.toString());
|
|
368
436
|
});
|
|
369
|
-
// Handle process exit
|
|
370
437
|
child.on('exit', (code) => {
|
|
371
|
-
console.log(`[MessageHandler] Claude process exited with code: ${code}`);
|
|
372
438
|
if (code === 0) {
|
|
373
439
|
const output = chunks.join('');
|
|
374
|
-
this.sendResponse(messageId, {
|
|
440
|
+
this.sendResponse(messageId, threadId, {
|
|
375
441
|
success: true,
|
|
376
442
|
output: output.trim() || '✅ Command executed successfully',
|
|
377
443
|
});
|
|
378
444
|
}
|
|
379
445
|
else {
|
|
380
446
|
const errorOutput = errorChunks.join('') || chunks.join('');
|
|
381
|
-
this.sendResponse(messageId, {
|
|
447
|
+
this.sendResponse(messageId, threadId, {
|
|
382
448
|
success: false,
|
|
383
449
|
error: errorOutput.trim() || `Command failed with exit code ${code}`,
|
|
384
450
|
});
|
|
385
451
|
}
|
|
386
452
|
resolve();
|
|
387
453
|
});
|
|
388
|
-
// Handle process error
|
|
389
454
|
child.on('error', (error) => {
|
|
390
|
-
console.error(
|
|
391
|
-
this.sendResponse(messageId, {
|
|
455
|
+
console.error('[MessageHandler] Failed to spawn Claude:', error);
|
|
456
|
+
this.sendResponse(messageId, threadId, {
|
|
392
457
|
success: false,
|
|
393
458
|
error: `Failed to execute command: ${error.message}`,
|
|
394
459
|
});
|
|
@@ -397,80 +462,58 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
397
462
|
});
|
|
398
463
|
}
|
|
399
464
|
/**
|
|
400
|
-
* Execute
|
|
465
|
+
* Execute AI command on a specific executor
|
|
401
466
|
*/
|
|
402
|
-
async executeCommand(messageId, content) {
|
|
467
|
+
async executeCommand(messageId, threadId, content, executor) {
|
|
403
468
|
try {
|
|
404
|
-
const result = await
|
|
405
|
-
onStream: (chunk) =>
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
},
|
|
411
|
-
onToolResult: (toolResult) => {
|
|
412
|
-
this.sendToolResult(messageId, toolResult);
|
|
413
|
-
},
|
|
414
|
-
onRedactedThinking: () => {
|
|
415
|
-
this.sendRedactedThinking(messageId);
|
|
416
|
-
},
|
|
417
|
-
onPlanMode: (planContent) => {
|
|
418
|
-
this.sendPlanMode(messageId, planContent);
|
|
419
|
-
},
|
|
469
|
+
const result = await executor.execute(content, {
|
|
470
|
+
onStream: (chunk) => this.sendStreamChunk(messageId, threadId, chunk),
|
|
471
|
+
onToolUse: (toolUse) => this.sendToolUse(messageId, threadId, toolUse),
|
|
472
|
+
onToolResult: (toolResult) => this.sendToolResult(messageId, threadId, toolResult),
|
|
473
|
+
onRedactedThinking: () => this.sendRedactedThinking(messageId, threadId),
|
|
474
|
+
onPlanMode: (planContent) => this.sendPlanMode(messageId, threadId, planContent),
|
|
420
475
|
});
|
|
421
|
-
// Only send success status, not the output
|
|
422
|
-
// Output has already been streamed via onStream callback
|
|
423
476
|
if (!result.success && result.error && result.error.includes('Prompt too long')) {
|
|
424
|
-
if ('compactWhenFull' in
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const compactResult = await persistentExecutor.compactWhenFull((chunk) => {
|
|
429
|
-
this.sendStreamChunk(messageId, chunk);
|
|
477
|
+
if ('compactWhenFull' in executor && typeof executor.compactWhenFull === 'function') {
|
|
478
|
+
this.sendStreamChunk(messageId, threadId, '⚠️ Conversation history too long, auto-compressing...\n');
|
|
479
|
+
const compactResult = await executor.compactWhenFull((chunk) => {
|
|
480
|
+
this.sendStreamChunk(messageId, threadId, chunk);
|
|
430
481
|
});
|
|
431
482
|
if (!compactResult.success) {
|
|
432
|
-
this.sendResponse(messageId, {
|
|
483
|
+
this.sendResponse(messageId, threadId, {
|
|
433
484
|
success: false,
|
|
434
485
|
error: `❌ Auto-compact failed: ${compactResult.error}\n\nUse /compact to try again, or /clear to start fresh.`,
|
|
435
486
|
});
|
|
436
487
|
return;
|
|
437
488
|
}
|
|
438
|
-
this.sendStreamChunk(messageId, '✅ Compressed. Retrying...\n');
|
|
439
|
-
const retryResult = await
|
|
440
|
-
onStream: (chunk) =>
|
|
441
|
-
onToolUse: (toolUse) =>
|
|
442
|
-
onToolResult: (toolResult) =>
|
|
443
|
-
onRedactedThinking: () =>
|
|
444
|
-
onPlanMode: (planContent) =>
|
|
445
|
-
});
|
|
446
|
-
this.sendResponse(messageId, {
|
|
447
|
-
success: retryResult.success,
|
|
448
|
-
error: retryResult.error,
|
|
489
|
+
this.sendStreamChunk(messageId, threadId, '✅ Compressed. Retrying...\n');
|
|
490
|
+
const retryResult = await executor.execute(content, {
|
|
491
|
+
onStream: (chunk) => this.sendStreamChunk(messageId, threadId, chunk),
|
|
492
|
+
onToolUse: (toolUse) => this.sendToolUse(messageId, threadId, toolUse),
|
|
493
|
+
onToolResult: (toolResult) => this.sendToolResult(messageId, threadId, toolResult),
|
|
494
|
+
onRedactedThinking: () => this.sendRedactedThinking(messageId, threadId),
|
|
495
|
+
onPlanMode: (planContent) => this.sendPlanMode(messageId, threadId, planContent),
|
|
449
496
|
});
|
|
497
|
+
this.sendResponse(messageId, threadId, { success: retryResult.success, error: retryResult.error, threads: this.threadPool.getSummaries() });
|
|
450
498
|
return;
|
|
451
499
|
}
|
|
452
|
-
this.sendResponse(messageId, {
|
|
500
|
+
this.sendResponse(messageId, threadId, {
|
|
453
501
|
success: false,
|
|
454
502
|
error: '❌ Conversation history too long.\n\nUse /compact to compress it, or /clear to start fresh.',
|
|
455
503
|
});
|
|
456
504
|
return;
|
|
457
505
|
}
|
|
458
|
-
this.sendResponse(messageId, {
|
|
459
|
-
success: result.success,
|
|
460
|
-
error: result.error,
|
|
461
|
-
});
|
|
506
|
+
this.sendResponse(messageId, threadId, { success: result.success, error: result.error, threads: this.threadPool.getSummaries() });
|
|
462
507
|
}
|
|
463
508
|
catch (error) {
|
|
464
|
-
this.sendResponse(messageId, {
|
|
509
|
+
this.sendResponse(messageId, threadId, {
|
|
465
510
|
success: false,
|
|
466
511
|
error: error instanceof Error ? error.message : 'Execution error',
|
|
467
512
|
});
|
|
468
513
|
}
|
|
469
514
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
*/
|
|
473
|
-
sendStreamChunk(messageId, chunk) {
|
|
515
|
+
// ── Outgoing message helpers ──────────────────────────────────────────────
|
|
516
|
+
sendStreamChunk(messageId, threadId, chunk) {
|
|
474
517
|
try {
|
|
475
518
|
this.wsClient.send({
|
|
476
519
|
type: 'stream',
|
|
@@ -478,18 +521,15 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
478
521
|
chunk,
|
|
479
522
|
streamType: 'text',
|
|
480
523
|
openId: this.currentOpenId,
|
|
524
|
+
threadId,
|
|
481
525
|
timestamp: Date.now(),
|
|
482
526
|
});
|
|
483
527
|
}
|
|
484
528
|
catch (error) {
|
|
485
|
-
// Ignore send errors, don't affect main flow
|
|
486
529
|
console.error('Failed to send stream chunk:', error);
|
|
487
530
|
}
|
|
488
531
|
}
|
|
489
|
-
|
|
490
|
-
* Send tool use event
|
|
491
|
-
*/
|
|
492
|
-
sendToolUse(messageId, toolUse) {
|
|
532
|
+
sendToolUse(messageId, threadId, toolUse) {
|
|
493
533
|
try {
|
|
494
534
|
this.wsClient.send({
|
|
495
535
|
type: 'stream',
|
|
@@ -497,6 +537,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
497
537
|
streamType: 'tool_use',
|
|
498
538
|
toolUse,
|
|
499
539
|
openId: this.currentOpenId,
|
|
540
|
+
threadId,
|
|
500
541
|
timestamp: Date.now(),
|
|
501
542
|
});
|
|
502
543
|
}
|
|
@@ -504,10 +545,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
504
545
|
console.error('Failed to send tool use:', error);
|
|
505
546
|
}
|
|
506
547
|
}
|
|
507
|
-
|
|
508
|
-
* Send tool result event
|
|
509
|
-
*/
|
|
510
|
-
sendToolResult(messageId, toolResult) {
|
|
548
|
+
sendToolResult(messageId, threadId, toolResult) {
|
|
511
549
|
try {
|
|
512
550
|
this.wsClient.send({
|
|
513
551
|
type: 'stream',
|
|
@@ -515,6 +553,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
515
553
|
streamType: 'tool_result',
|
|
516
554
|
toolResult,
|
|
517
555
|
openId: this.currentOpenId,
|
|
556
|
+
threadId,
|
|
518
557
|
timestamp: Date.now(),
|
|
519
558
|
});
|
|
520
559
|
}
|
|
@@ -522,17 +561,14 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
522
561
|
console.error('Failed to send tool result:', error);
|
|
523
562
|
}
|
|
524
563
|
}
|
|
525
|
-
|
|
526
|
-
* Send redacted thinking event
|
|
527
|
-
* This occurs when AI reasoning is filtered by safety systems (Claude 3.7 Sonnet, Gemini)
|
|
528
|
-
*/
|
|
529
|
-
sendRedactedThinking(messageId) {
|
|
564
|
+
sendRedactedThinking(messageId, threadId) {
|
|
530
565
|
try {
|
|
531
566
|
this.wsClient.send({
|
|
532
567
|
type: 'stream',
|
|
533
568
|
messageId,
|
|
534
569
|
streamType: 'redacted_thinking',
|
|
535
570
|
openId: this.currentOpenId,
|
|
571
|
+
threadId,
|
|
536
572
|
timestamp: Date.now(),
|
|
537
573
|
});
|
|
538
574
|
}
|
|
@@ -540,12 +576,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
540
576
|
console.error('Failed to send redacted thinking:', error);
|
|
541
577
|
}
|
|
542
578
|
}
|
|
543
|
-
|
|
544
|
-
* Send plan mode event
|
|
545
|
-
* Fired when Claude completes its plan between EnterPlanMode and ExitPlanMode tool calls.
|
|
546
|
-
* Execution is auto-approved; this event is for user visibility only.
|
|
547
|
-
*/
|
|
548
|
-
sendPlanMode(messageId, planContent) {
|
|
579
|
+
sendPlanMode(messageId, threadId, planContent) {
|
|
549
580
|
try {
|
|
550
581
|
this.wsClient.send({
|
|
551
582
|
type: 'stream',
|
|
@@ -553,6 +584,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
553
584
|
streamType: 'plan_mode',
|
|
554
585
|
planContent,
|
|
555
586
|
openId: this.currentOpenId,
|
|
587
|
+
threadId,
|
|
556
588
|
timestamp: Date.now(),
|
|
557
589
|
});
|
|
558
590
|
}
|
|
@@ -560,29 +592,33 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
560
592
|
console.error('Failed to send plan mode:', error);
|
|
561
593
|
}
|
|
562
594
|
}
|
|
563
|
-
|
|
564
|
-
* Send structured content for rich formatting
|
|
565
|
-
*/
|
|
566
|
-
sendStructuredContent(messageId, structuredContent) {
|
|
595
|
+
sendStructuredContent(messageId, threadId, structuredContent) {
|
|
567
596
|
try {
|
|
568
597
|
this.wsClient.send({
|
|
569
598
|
type: 'structured',
|
|
570
599
|
messageId,
|
|
571
600
|
structuredContent,
|
|
572
601
|
openId: this.currentOpenId,
|
|
602
|
+
threadId,
|
|
573
603
|
timestamp: Date.now(),
|
|
574
|
-
cwd: this.executor.getCurrentWorkingDirectory(),
|
|
575
604
|
});
|
|
576
605
|
}
|
|
577
606
|
catch (error) {
|
|
578
607
|
console.error('Failed to send structured content:', error);
|
|
579
608
|
}
|
|
580
609
|
}
|
|
581
|
-
|
|
582
|
-
* Send response
|
|
583
|
-
*/
|
|
584
|
-
sendResponse(messageId, result) {
|
|
610
|
+
sendResponse(messageId, threadId, result) {
|
|
585
611
|
try {
|
|
612
|
+
// Resolve CWD from thread executor if possible
|
|
613
|
+
let cwd;
|
|
614
|
+
try {
|
|
615
|
+
if (threadId) {
|
|
616
|
+
cwd = this.threadPool.getExecutor(threadId).getCurrentWorkingDirectory();
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
// Ignore — thread may have been deleted
|
|
621
|
+
}
|
|
586
622
|
this.wsClient.send({
|
|
587
623
|
type: 'response',
|
|
588
624
|
messageId,
|
|
@@ -591,25 +627,22 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
591
627
|
error: result.error,
|
|
592
628
|
sessionAbbr: result.sessionAbbr,
|
|
593
629
|
openId: this.currentOpenId,
|
|
630
|
+
threadId,
|
|
631
|
+
threads: result.threads,
|
|
632
|
+
cwd,
|
|
594
633
|
timestamp: Date.now(),
|
|
595
|
-
cwd: this.executor.getCurrentWorkingDirectory(),
|
|
596
634
|
});
|
|
597
635
|
}
|
|
598
636
|
catch (error) {
|
|
599
637
|
console.error('Failed to send response:', error);
|
|
600
638
|
}
|
|
601
639
|
}
|
|
602
|
-
|
|
603
|
-
* Detect whether a command is available on PATH
|
|
604
|
-
*/
|
|
640
|
+
// ── Backend switching ─────────────────────────────────────────────────────
|
|
605
641
|
checkCommand(cmd, args) {
|
|
606
642
|
return new Promise((resolve) => {
|
|
607
643
|
(0, child_process_1.execFile)(cmd, args, { timeout: 5000 }, (err) => resolve(!err));
|
|
608
644
|
});
|
|
609
645
|
}
|
|
610
|
-
/**
|
|
611
|
-
* Detect all installed AI backends
|
|
612
|
-
*/
|
|
613
646
|
async detectBackends() {
|
|
614
647
|
const [claudeInstalled, geminiInstalled] = await Promise.all([
|
|
615
648
|
this.checkCommand('claude', ['--version']),
|
|
@@ -620,23 +653,16 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
620
653
|
{ id: 'gemini', label: 'Gemini CLI', installed: geminiInstalled },
|
|
621
654
|
];
|
|
622
655
|
}
|
|
623
|
-
|
|
624
|
-
* Handle /backend command
|
|
625
|
-
* Usage:
|
|
626
|
-
* /backend — list available backends
|
|
627
|
-
* /backend <name|index> — switch to the specified backend
|
|
628
|
-
*/
|
|
629
|
-
async handleBackendCommand(messageId, trimmed) {
|
|
656
|
+
async handleBackendCommand(messageId, threadId, trimmed) {
|
|
630
657
|
const parts = trimmed.split(/\s+/);
|
|
631
658
|
const arg = parts[1];
|
|
632
659
|
const currentConfig = this.config.get('executor') ?? { type: 'auto' };
|
|
633
660
|
const currentType = currentConfig.type;
|
|
634
661
|
const backends = await this.detectBackends();
|
|
635
662
|
const installed = backends.filter((b) => b.installed);
|
|
636
|
-
// List mode
|
|
637
663
|
if (!arg) {
|
|
638
664
|
if (installed.length === 0) {
|
|
639
|
-
this.sendResponse(messageId, {
|
|
665
|
+
this.sendResponse(messageId, threadId, {
|
|
640
666
|
success: false,
|
|
641
667
|
error: 'No supported AI backends found.\n\nMake sure Claude Code is installed: npm install -g @anthropic-ai/claude-code',
|
|
642
668
|
});
|
|
@@ -648,13 +674,12 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
648
674
|
const active = b.id === currentType || isClaudeActive ? ' ★ (active)' : '';
|
|
649
675
|
return `${i + 1}. ${b.label}${active}`;
|
|
650
676
|
});
|
|
651
|
-
this.sendResponse(messageId, {
|
|
677
|
+
this.sendResponse(messageId, threadId, {
|
|
652
678
|
success: true,
|
|
653
679
|
output: `🤖 Available AI backends:\n${lines.join('\n')}\n\nSwitch with: /backend <index> or /backend <name>`,
|
|
654
680
|
});
|
|
655
681
|
return;
|
|
656
682
|
}
|
|
657
|
-
// Switch mode — resolve by index or name
|
|
658
683
|
const index = parseInt(arg, 10);
|
|
659
684
|
let target;
|
|
660
685
|
if (!isNaN(index) && index >= 1 && index <= installed.length) {
|
|
@@ -664,33 +689,32 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
664
689
|
target = installed.find((b) => b.id === arg || b.label.toLowerCase().includes(arg.toLowerCase()));
|
|
665
690
|
}
|
|
666
691
|
if (!target) {
|
|
667
|
-
this.sendResponse(messageId, {
|
|
692
|
+
this.sendResponse(messageId, threadId, {
|
|
668
693
|
success: false,
|
|
669
694
|
error: `Backend "${arg}" not found. Use /backend to see available options.`,
|
|
670
695
|
});
|
|
671
696
|
return;
|
|
672
697
|
}
|
|
673
|
-
// Persist to config
|
|
674
698
|
const newConfig = { ...currentConfig, type: target.id };
|
|
675
699
|
await this.config.set('executor', newConfig);
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
if (typeof this.executor.destroy === 'function') {
|
|
679
|
-
this.executor.destroy();
|
|
680
|
-
}
|
|
681
|
-
this.executor = (0, executor_1.createExecutor)(this.directoryGuard, newConfig, cwd);
|
|
682
|
-
this.sendResponse(messageId, {
|
|
700
|
+
await this.threadPool.switchBackend(newConfig);
|
|
701
|
+
this.sendResponse(messageId, threadId, {
|
|
683
702
|
success: true,
|
|
684
|
-
output: `✅ Backend switched to: ${target.label}
|
|
703
|
+
output: `✅ Backend switched to: ${target.label}\n\nAll threads will use the new backend for future commands.`,
|
|
685
704
|
});
|
|
686
705
|
}
|
|
687
706
|
/**
|
|
688
|
-
* Destroy handler
|
|
707
|
+
* Destroy handler and all executors
|
|
689
708
|
*/
|
|
690
|
-
destroy() {
|
|
709
|
+
async destroy() {
|
|
691
710
|
this.isDestroyed = true;
|
|
692
|
-
this.isExecuting = false;
|
|
693
711
|
this.notificationAdapter.unregister();
|
|
712
|
+
try {
|
|
713
|
+
await this.threadPool.destroyAll();
|
|
714
|
+
}
|
|
715
|
+
catch (err) {
|
|
716
|
+
console.error('Error destroying thread executors:', err);
|
|
717
|
+
}
|
|
694
718
|
}
|
|
695
719
|
}
|
|
696
720
|
exports.MessageHandler = MessageHandler;
|