@yu_robotics/remote-cli 1.6.8 → 1.6.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.
- package/README.md +11 -3
- package/dist/client/MessageHandler.d.ts +14 -1
- package/dist/client/MessageHandler.d.ts.map +1 -1
- package/dist/client/MessageHandler.js +405 -39
- package/dist/client/MessageHandler.js.map +1 -1
- package/dist/thread/ThreadExecutorPool.d.ts +9 -2
- package/dist/thread/ThreadExecutorPool.d.ts.map +1 -1
- package/dist/thread/ThreadExecutorPool.js +39 -2
- package/dist/thread/ThreadExecutorPool.js.map +1 -1
- package/dist/thread/ThreadManager.d.ts +5 -2
- package/dist/thread/ThreadManager.d.ts.map +1 -1
- package/dist/thread/ThreadManager.js +14 -1
- package/dist/thread/ThreadManager.js.map +1 -1
- package/dist/thread/types.d.ts +3 -0
- package/dist/thread/types.d.ts.map +1 -1
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -5,7 +5,13 @@ const hooks_1 = require("../hooks");
|
|
|
5
5
|
const FileReadDetector_1 = require("../utils/FileReadDetector");
|
|
6
6
|
const MachineCommands_1 = require("../machines/MachineCommands");
|
|
7
7
|
const child_process_1 = require("child_process");
|
|
8
|
+
const promises_1 = require("fs/promises");
|
|
9
|
+
const os_1 = require("os");
|
|
10
|
+
const path_1 = require("path");
|
|
8
11
|
const config_1 = require("../types/config");
|
|
12
|
+
const uuid_1 = require("uuid");
|
|
13
|
+
const QUEUE_CONFIRMATION_TTL_MS = 5 * 60 * 1000;
|
|
14
|
+
const MAX_THREAD_QUEUE_LENGTH = 10;
|
|
9
15
|
/**
|
|
10
16
|
* Thread-aware Message Handler.
|
|
11
17
|
* Each thread has its own executor process managed by ThreadExecutorPool.
|
|
@@ -22,6 +28,10 @@ class MessageHandler {
|
|
|
22
28
|
notificationAdapter;
|
|
23
29
|
machineCommands;
|
|
24
30
|
pendingReplaces = new Map();
|
|
31
|
+
threadQueues = new Map();
|
|
32
|
+
pendingQueueConfirmations = new Map();
|
|
33
|
+
pausedQueues = new Set();
|
|
34
|
+
messageOpenIds = new Map();
|
|
25
35
|
constructor(wsClient, threadPool, threadManager, directoryGuard, config) {
|
|
26
36
|
this.wsClient = wsClient;
|
|
27
37
|
this.threadPool = threadPool;
|
|
@@ -71,6 +81,7 @@ class MessageHandler {
|
|
|
71
81
|
async handleCommandMessage(message) {
|
|
72
82
|
const { messageId, content, attachments, workingDirectory, openId, isSlashCommand, threadId } = message;
|
|
73
83
|
this.currentOpenId = openId;
|
|
84
|
+
this.messageOpenIds.set(message.messageId, openId);
|
|
74
85
|
this.notificationAdapter.setCurrentOpenId(openId);
|
|
75
86
|
// Resolve target thread — fall back to default if not specified
|
|
76
87
|
const thread = threadId
|
|
@@ -85,6 +96,23 @@ class MessageHandler {
|
|
|
85
96
|
}
|
|
86
97
|
const resolvedThreadId = thread.id;
|
|
87
98
|
const executor = this.threadPool.getExecutor(resolvedThreadId);
|
|
99
|
+
if (content?.trim() === '/queue' || content?.trim().startsWith('/queue ')) {
|
|
100
|
+
await this.handleQueueCommand(messageId, resolvedThreadId, content.trim());
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Backend switching has its own lifecycle handling and must run before the
|
|
104
|
+
// normal command busy flag is acquired.
|
|
105
|
+
if (content?.trim() === '/backend' || content?.trim().startsWith('/backend ')) {
|
|
106
|
+
if (this.threadPool.isThreadBusy(resolvedThreadId)) {
|
|
107
|
+
this.sendResponse(messageId, resolvedThreadId, {
|
|
108
|
+
success: false,
|
|
109
|
+
error: `Thread "${thread.name}" is busy. Send /abort to cancel the running task before switching backend.`,
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
await this.handleBackendCommand(messageId, resolvedThreadId, content.trim());
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
88
116
|
// Handle /abort for this specific thread (bypasses busy check)
|
|
89
117
|
if (content?.trim() === '/abort') {
|
|
90
118
|
await this.handleAbortCommand(messageId, resolvedThreadId, executor);
|
|
@@ -120,9 +148,35 @@ class MessageHandler {
|
|
|
120
148
|
await this.executePendingReplace(messageId, resolvedThreadId, pendingKey, content);
|
|
121
149
|
return;
|
|
122
150
|
}
|
|
151
|
+
if (isSlashCommand || content?.trim().startsWith('/')) {
|
|
152
|
+
this.sendResponse(messageId, resolvedThreadId, {
|
|
153
|
+
success: false,
|
|
154
|
+
error: `Thread "${thread.name}" is busy. Send /abort to cancel the running task, or use /queue to inspect queued messages.`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
if ((this.threadQueues.get(resolvedThreadId)?.length ?? 0) >= MAX_THREAD_QUEUE_LENGTH) {
|
|
159
|
+
this.sendResponse(messageId, resolvedThreadId, {
|
|
160
|
+
success: false,
|
|
161
|
+
error: `Thread queue is full (${MAX_THREAD_QUEUE_LENGTH} messages). Use /abort to clear it.`,
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const queueConfirmation = this.createQueueConfirmation(resolvedThreadId, thread.name, content, attachments, openId, messageId, executor);
|
|
166
|
+
this.sendResponse(messageId, resolvedThreadId, {
|
|
167
|
+
success: false,
|
|
168
|
+
output: `⏳ Thread "${thread.name}" is busy. Confirm whether to add this message to its queue.`,
|
|
169
|
+
queueConfirmation,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const queueSensitiveCommand = /^\/(?:clear|compact|cd|model|effort)(?:\s|$)/.test(content?.trim() ?? '')
|
|
175
|
+
|| /^\/thread\s+delete(?:\s|$)/.test(content?.trim() ?? '');
|
|
176
|
+
if (queueSensitiveCommand && this.hasThreadQueueState(resolvedThreadId)) {
|
|
123
177
|
this.sendResponse(messageId, resolvedThreadId, {
|
|
124
178
|
success: false,
|
|
125
|
-
error:
|
|
179
|
+
error: 'This thread has queued messages. Send /abort to clear the current task and queue before changing its execution context.',
|
|
126
180
|
});
|
|
127
181
|
return;
|
|
128
182
|
}
|
|
@@ -160,7 +214,10 @@ class MessageHandler {
|
|
|
160
214
|
}
|
|
161
215
|
const expandedContent = this.expandCommandShortcuts(content);
|
|
162
216
|
const processedContent = (0, FileReadDetector_1.processFileReadContent)(expandedContent);
|
|
163
|
-
await this.executeCommand(messageId, resolvedThreadId, processedContent, executor, attachments);
|
|
217
|
+
const success = await this.executeCommand(messageId, resolvedThreadId, processedContent, executor, attachments);
|
|
218
|
+
if (!success && this.threadQueues.has(resolvedThreadId)) {
|
|
219
|
+
this.pausedQueues.add(resolvedThreadId);
|
|
220
|
+
}
|
|
164
221
|
}
|
|
165
222
|
catch (error) {
|
|
166
223
|
this.threadPool.setThreadError(resolvedThreadId, true);
|
|
@@ -171,6 +228,8 @@ class MessageHandler {
|
|
|
171
228
|
}
|
|
172
229
|
finally {
|
|
173
230
|
this.threadPool.setThreadBusy(resolvedThreadId, false);
|
|
231
|
+
if (!this.pausedQueues.has(resolvedThreadId))
|
|
232
|
+
void this.startNextQueuedCommand(resolvedThreadId);
|
|
174
233
|
}
|
|
175
234
|
}
|
|
176
235
|
/**
|
|
@@ -178,20 +237,21 @@ class MessageHandler {
|
|
|
178
237
|
*/
|
|
179
238
|
async handleAbortCommand(messageId, threadId, executor) {
|
|
180
239
|
const wasExecuting = this.threadPool.isThreadBusy(threadId);
|
|
240
|
+
const clearedCount = this.clearThreadQueue(threadId);
|
|
181
241
|
const aborted = await executor.abort();
|
|
182
242
|
if (aborted) {
|
|
183
243
|
this.threadPool.setThreadBusy(threadId, false);
|
|
184
244
|
this.sendResponse(messageId, threadId, {
|
|
185
245
|
success: true,
|
|
186
|
-
output: wasExecuting
|
|
187
|
-
? '✅ Current command has been aborted'
|
|
188
|
-
: '⚠️ No command was executing, but executor has been reset',
|
|
246
|
+
output: `${wasExecuting ? '✅ Current command has been aborted' : '⚠️ No command was executing, but executor has been reset'}${clearedCount ? `\n🗑️ Cleared ${clearedCount} queued message${clearedCount === 1 ? '' : 's'}.` : ''}`,
|
|
189
247
|
});
|
|
190
248
|
}
|
|
191
249
|
else {
|
|
192
250
|
this.sendResponse(messageId, threadId, {
|
|
193
251
|
success: true,
|
|
194
|
-
output:
|
|
252
|
+
output: clearedCount
|
|
253
|
+
? `ℹ️ No command is currently executing\n🗑️ Cleared ${clearedCount} queued message${clearedCount === 1 ? '' : 's'}.`
|
|
254
|
+
: 'ℹ️ No command is currently executing',
|
|
195
255
|
});
|
|
196
256
|
}
|
|
197
257
|
}
|
|
@@ -231,12 +291,24 @@ class MessageHandler {
|
|
|
231
291
|
const cwd = executor.getCurrentWorkingDirectory();
|
|
232
292
|
const allowedDirs = this.directoryGuard.getAllowedDirectories();
|
|
233
293
|
const threads = this.threadPool.getSummaries();
|
|
294
|
+
const thread = this.threadManager.getThread(threadId);
|
|
295
|
+
const backend = this.threadPool.getBackendKey(threadId);
|
|
296
|
+
const model = thread?.models?.[backend] ?? (backend === 'claude' ? thread?.model : undefined);
|
|
297
|
+
const effort = thread?.efforts?.[backend] ?? 'auto';
|
|
234
298
|
const threadList = threads
|
|
235
|
-
.map(t =>
|
|
299
|
+
.map(t => {
|
|
300
|
+
const queued = this.threadQueues.get(t.id)?.length ?? 0;
|
|
301
|
+
const queueSuffix = queued > 0 ? `, queue: ${queued}` : '';
|
|
302
|
+
return ` • ${t.name}${t.status === 'running' ? ' 🔄' : t.status === 'error' ? ' ❌' : ' ✅'} (${t.status}, backend: ${t.backend ?? 'claude'}${queueSuffix})`;
|
|
303
|
+
})
|
|
236
304
|
.join('\n');
|
|
237
305
|
this.sendResponse(messageId, threadId, {
|
|
238
306
|
success: true,
|
|
239
307
|
output: `📊 Status:
|
|
308
|
+
- Thread: ${thread?.name ?? threadId}
|
|
309
|
+
- Backend: ${backend}
|
|
310
|
+
- Model: ${model ?? 'backend default'}
|
|
311
|
+
- Reasoning effort: ${effort}
|
|
240
312
|
- Working Directory: ${cwd}
|
|
241
313
|
- Allowed Directories: ${allowedDirs.join(', ')}
|
|
242
314
|
- Connection: Active
|
|
@@ -244,19 +316,66 @@ class MessageHandler {
|
|
|
244
316
|
});
|
|
245
317
|
return true;
|
|
246
318
|
}
|
|
319
|
+
if (trimmed === '/context') {
|
|
320
|
+
const thread = this.threadManager.getThread(threadId);
|
|
321
|
+
const backend = this.threadPool.getBackendKey(threadId);
|
|
322
|
+
const model = thread?.models?.[backend] ?? (backend === 'claude' ? thread?.model : undefined);
|
|
323
|
+
const effort = thread?.efforts?.[backend] ?? 'auto';
|
|
324
|
+
const sessionId = typeof executor.getSessionId === 'function' ? executor.getSessionId() : null;
|
|
325
|
+
const queued = this.threadQueues.get(threadId)?.length ?? 0;
|
|
326
|
+
const pending = Array.from(this.pendingQueueConfirmations.values())
|
|
327
|
+
.filter((confirmation) => confirmation.info.threadId === threadId).length;
|
|
328
|
+
this.sendResponse(messageId, threadId, {
|
|
329
|
+
success: true,
|
|
330
|
+
output: `🧠 Context:
|
|
331
|
+
- Thread: ${thread?.name ?? threadId}
|
|
332
|
+
- Backend: ${backend}
|
|
333
|
+
- Model: ${model ?? 'backend default'}
|
|
334
|
+
- Reasoning effort: ${effort}
|
|
335
|
+
- Working Directory: ${executor.getCurrentWorkingDirectory()}
|
|
336
|
+
- Session: ${sessionId ?? 'not started'}
|
|
337
|
+
- Queue: ${queued} confirmed, ${pending} awaiting confirmation
|
|
338
|
+
- Token usage: exact usage is not exposed by the active remote transport
|
|
339
|
+
|
|
340
|
+
Use /compact to reduce conversation context or /clear to start a fresh context.`,
|
|
341
|
+
});
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
if (trimmed === '/skills') {
|
|
345
|
+
const backend = this.threadPool.getBackendKey(threadId);
|
|
346
|
+
if (backend === 'claude' || backend === 'agy') {
|
|
347
|
+
await this.executeSlashCommand(messageId, threadId, trimmed, executor);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
const skills = await this.listLocalSkills(executor.getCurrentWorkingDirectory(), backend);
|
|
351
|
+
this.sendResponse(messageId, threadId, {
|
|
352
|
+
success: true,
|
|
353
|
+
output: skills.length > 0
|
|
354
|
+
? `🧩 Available skills (local discovery):\n${skills.map((skill) => `- ${skill}`).join('\n')}`
|
|
355
|
+
: '🧩 No local skills were discovered for the Codex backend.\n\nExpected locations include .agents/skills and ~/.codex/skills.',
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
247
360
|
if (trimmed === '/help') {
|
|
248
361
|
this.sendResponse(messageId, threadId, {
|
|
249
362
|
success: true,
|
|
250
363
|
output: `📖 Available commands:
|
|
251
364
|
- /help - Show this help message
|
|
252
|
-
- /status - Show
|
|
365
|
+
- /status - Show backend, model, effort, queue, and thread status
|
|
366
|
+
- /context - Show current session context and queue diagnostics
|
|
367
|
+
- /skills - List available skills for the active backend
|
|
253
368
|
- /abort - Abort the currently executing command in this thread
|
|
369
|
+
- /queue [clear|continue|confirm <id>|cancel <id>] - Inspect or manage this thread's command queue
|
|
254
370
|
- /clear - Clear conversation context for this thread
|
|
255
371
|
- /compact - Compress conversation history to reduce context size
|
|
256
372
|
- /cd <directory> - Change working directory for this thread
|
|
257
|
-
- /model [name] - Show
|
|
258
|
-
- /effort [auto|
|
|
259
|
-
- /backend - List
|
|
373
|
+
- /model [name] - Show models for the active backend, or set this thread's model
|
|
374
|
+
- /effort [auto|low|medium|high] - Show or set effort for Codex/AGY (Claude Code is unsupported)
|
|
375
|
+
- /backend - List backends and show the current thread's effective backend
|
|
376
|
+
- /backend <index> - Switch all threads and clear per-thread backend overrides
|
|
377
|
+
- /backend <index> @ - Switch only the current thread
|
|
378
|
+
- /backend default @ - Clear the current thread override and follow the global backend
|
|
260
379
|
- /thread list - List all threads with their status
|
|
261
380
|
- /thread new [name] - Create a new thread
|
|
262
381
|
- /thread delete <name> - Delete a thread (only when idle)
|
|
@@ -340,7 +459,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
340
459
|
}
|
|
341
460
|
if (trimmed === '/effort' || trimmed.startsWith('/effort ')) {
|
|
342
461
|
const executorConfig = this.config.get('executor') ?? { type: 'auto' };
|
|
343
|
-
const key =
|
|
462
|
+
const key = this.threadPool.getBackendKey(threadId);
|
|
344
463
|
if (key !== 'codex' && key !== 'agy') {
|
|
345
464
|
this.sendResponse(messageId, threadId, {
|
|
346
465
|
success: false,
|
|
@@ -432,7 +551,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
432
551
|
// thread.models[backendKey]. The legacy `model` field is kept in
|
|
433
552
|
// sync for the Claude backend only.
|
|
434
553
|
const executorConfig = this.config.get('executor') ?? { type: 'auto' };
|
|
435
|
-
const key =
|
|
554
|
+
const key = this.threadPool.getBackendKey(threadId);
|
|
436
555
|
const current = this.threadManager.getThread(threadId);
|
|
437
556
|
const models = { ...current?.models, [key]: modelArg };
|
|
438
557
|
await this.threadManager.updateThread(threadId, key === 'claude' ? { models, model: modelArg } : { models });
|
|
@@ -449,6 +568,170 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
449
568
|
}
|
|
450
569
|
return false;
|
|
451
570
|
}
|
|
571
|
+
createQueueConfirmation(threadId, threadName, content, attachments, openId, messageId, executor) {
|
|
572
|
+
this.removeExpiredQueueConfirmations();
|
|
573
|
+
const id = (0, uuid_1.v4)();
|
|
574
|
+
const info = {
|
|
575
|
+
id,
|
|
576
|
+
threadId,
|
|
577
|
+
threadName,
|
|
578
|
+
backend: this.threadPool.getBackendKey(threadId),
|
|
579
|
+
cwd: executor.getCurrentWorkingDirectory(),
|
|
580
|
+
preview: content.replace(/\s+/g, ' ').trim().slice(0, 240),
|
|
581
|
+
pendingCount: this.threadQueues.get(threadId)?.length ?? 0,
|
|
582
|
+
expiresAt: Date.now() + QUEUE_CONFIRMATION_TTL_MS,
|
|
583
|
+
};
|
|
584
|
+
this.pendingQueueConfirmations.set(id, {
|
|
585
|
+
info,
|
|
586
|
+
command: { messageId, threadId, content, attachments, openId, enqueuedAt: Date.now() },
|
|
587
|
+
});
|
|
588
|
+
setTimeout(() => {
|
|
589
|
+
const pending = this.pendingQueueConfirmations.get(id);
|
|
590
|
+
if (pending && pending.info.expiresAt <= Date.now())
|
|
591
|
+
this.pendingQueueConfirmations.delete(id);
|
|
592
|
+
}, QUEUE_CONFIRMATION_TTL_MS).unref?.();
|
|
593
|
+
return info;
|
|
594
|
+
}
|
|
595
|
+
removeExpiredQueueConfirmations() {
|
|
596
|
+
const now = Date.now();
|
|
597
|
+
for (const [id, pending] of this.pendingQueueConfirmations) {
|
|
598
|
+
if (pending.info.expiresAt <= now)
|
|
599
|
+
this.pendingQueueConfirmations.delete(id);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
clearThreadQueue(threadId) {
|
|
603
|
+
let cleared = this.threadQueues.get(threadId)?.length ?? 0;
|
|
604
|
+
this.threadQueues.delete(threadId);
|
|
605
|
+
for (const [id, pending] of this.pendingQueueConfirmations) {
|
|
606
|
+
if (pending.info.threadId === threadId) {
|
|
607
|
+
this.pendingQueueConfirmations.delete(id);
|
|
608
|
+
cleared += 1;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
this.pausedQueues.delete(threadId);
|
|
612
|
+
return cleared;
|
|
613
|
+
}
|
|
614
|
+
clearAllQueues() {
|
|
615
|
+
const threadIds = new Set([
|
|
616
|
+
...this.threadQueues.keys(),
|
|
617
|
+
...this.pausedQueues,
|
|
618
|
+
...Array.from(this.pendingQueueConfirmations.values()).map((pending) => pending.info.threadId),
|
|
619
|
+
]);
|
|
620
|
+
return Array.from(threadIds).reduce((total, threadId) => total + this.clearThreadQueue(threadId), 0);
|
|
621
|
+
}
|
|
622
|
+
async handleQueueCommand(messageId, threadId, trimmed) {
|
|
623
|
+
this.removeExpiredQueueConfirmations();
|
|
624
|
+
const parts = trimmed.split(/\s+/);
|
|
625
|
+
const action = parts[1]?.toLowerCase();
|
|
626
|
+
const id = parts[2];
|
|
627
|
+
if (action === 'confirm' && id) {
|
|
628
|
+
const pending = this.pendingQueueConfirmations.get(id);
|
|
629
|
+
const currentCwd = this.threadPool.getExecutor(threadId).getCurrentWorkingDirectory();
|
|
630
|
+
if (!pending || pending.info.threadId !== threadId || pending.info.expiresAt <= Date.now()
|
|
631
|
+
|| pending.info.backend !== this.threadPool.getBackendKey(threadId) || pending.info.cwd !== currentCwd
|
|
632
|
+
|| (pending.command.openId && pending.command.openId !== this.getMessageOpenId(messageId))) {
|
|
633
|
+
this.pendingQueueConfirmations.delete(id);
|
|
634
|
+
this.sendResponse(messageId, threadId, { success: false, error: 'Queue confirmation is no longer valid. The message was not queued.' });
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
this.pendingQueueConfirmations.delete(id);
|
|
638
|
+
const queue = this.threadQueues.get(threadId) ?? [];
|
|
639
|
+
if (queue.length >= MAX_THREAD_QUEUE_LENGTH) {
|
|
640
|
+
this.sendResponse(messageId, threadId, { success: false, error: `This thread queue is full (${MAX_THREAD_QUEUE_LENGTH} messages).` });
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
queue.push({ ...pending.command, messageId: parts[3] || pending.command.messageId });
|
|
644
|
+
this.threadQueues.set(threadId, queue);
|
|
645
|
+
this.sendResponse(messageId, threadId, {
|
|
646
|
+
success: true,
|
|
647
|
+
output: `✅ Added to queue for ${pending.info.threadName}.\nQueue position: ${queue.length}\nPending messages for this thread: ${queue.length}`,
|
|
648
|
+
});
|
|
649
|
+
void this.startNextQueuedCommand(threadId);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (action === 'cancel' && id) {
|
|
653
|
+
const pending = this.pendingQueueConfirmations.get(id);
|
|
654
|
+
if (pending?.info.threadId === threadId) {
|
|
655
|
+
this.pendingQueueConfirmations.delete(id);
|
|
656
|
+
this.sendResponse(messageId, threadId, { success: true, output: '❌ Message was not queued.' });
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
this.sendResponse(messageId, threadId, { success: false, error: 'Queue confirmation is no longer valid.' });
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (action === 'clear') {
|
|
664
|
+
const cleared = this.clearThreadQueue(threadId);
|
|
665
|
+
this.sendResponse(messageId, threadId, {
|
|
666
|
+
success: true,
|
|
667
|
+
output: cleared ? `🗑️ Cleared ${cleared} queued message${cleared === 1 ? '' : 's'}.` : 'ℹ️ This thread queue is empty.',
|
|
668
|
+
});
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (action === 'continue') {
|
|
672
|
+
this.pausedQueues.delete(threadId);
|
|
673
|
+
this.sendResponse(messageId, threadId, { success: true, output: '▶️ Queue resumed.' });
|
|
674
|
+
void this.startNextQueuedCommand(threadId);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const lines = ['📋 Thread queues:'];
|
|
678
|
+
for (const summary of this.threadPool.getSummaries()) {
|
|
679
|
+
const queued = this.threadQueues.get(summary.id) ?? [];
|
|
680
|
+
const pending = Array.from(this.pendingQueueConfirmations.values()).filter((item) => item.info.threadId === summary.id);
|
|
681
|
+
if (summary.status !== 'running' && queued.length === 0 && pending.length === 0 && !this.pausedQueues.has(summary.id))
|
|
682
|
+
continue;
|
|
683
|
+
lines.push(`\n${summary.name} (${summary.backend ?? 'global'}) — ${summary.status}`);
|
|
684
|
+
lines.push(` Confirmed: ${queued.length}; awaiting confirmation: ${pending.length}`);
|
|
685
|
+
queued.forEach((command, index) => lines.push(` ${index + 1}. ${command.content.replace(/\s+/g, ' ').slice(0, 160)}`));
|
|
686
|
+
if (this.pausedQueues.has(summary.id))
|
|
687
|
+
lines.push(' ⏸️ Paused after a failed task. Use /queue continue or /abort.');
|
|
688
|
+
}
|
|
689
|
+
if (lines.length === 1)
|
|
690
|
+
lines.push('No active or pending thread queues.');
|
|
691
|
+
this.sendResponse(messageId, threadId, { success: true, output: lines.join('\n') });
|
|
692
|
+
}
|
|
693
|
+
async startNextQueuedCommand(threadId) {
|
|
694
|
+
if (this.threadPool.isThreadBusy(threadId) || this.pausedQueues.has(threadId))
|
|
695
|
+
return;
|
|
696
|
+
const queue = this.threadQueues.get(threadId);
|
|
697
|
+
const command = queue?.shift();
|
|
698
|
+
if (!command) {
|
|
699
|
+
this.threadQueues.delete(threadId);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (queue?.length === 0)
|
|
703
|
+
this.threadQueues.delete(threadId);
|
|
704
|
+
const thread = this.threadManager.getThread(threadId);
|
|
705
|
+
if (!thread)
|
|
706
|
+
return;
|
|
707
|
+
this.currentOpenId = command.openId;
|
|
708
|
+
this.messageOpenIds.set(command.messageId, command.openId);
|
|
709
|
+
this.notificationAdapter.setCurrentOpenId(command.openId);
|
|
710
|
+
this.threadPool.setThreadBusy(threadId, true);
|
|
711
|
+
try {
|
|
712
|
+
const executor = this.threadPool.getExecutor(threadId);
|
|
713
|
+
await this.threadManager.updateThread(threadId, { lastActiveAt: Date.now() });
|
|
714
|
+
const processedContent = (0, FileReadDetector_1.processFileReadContent)(this.expandCommandShortcuts(command.content));
|
|
715
|
+
const success = await this.executeCommand(command.messageId, threadId, processedContent, executor, command.attachments);
|
|
716
|
+
if (!success) {
|
|
717
|
+
this.pausedQueues.add(threadId);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
this.pausedQueues.add(threadId);
|
|
722
|
+
this.sendResponse(command.messageId, threadId, { success: false, error: error instanceof Error ? error.message : 'Queued command failed' });
|
|
723
|
+
}
|
|
724
|
+
finally {
|
|
725
|
+
this.threadPool.setThreadBusy(threadId, false);
|
|
726
|
+
if (!this.pausedQueues.has(threadId))
|
|
727
|
+
void this.startNextQueuedCommand(threadId);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
hasThreadQueueState(threadId) {
|
|
731
|
+
if ((this.threadQueues.get(threadId)?.length ?? 0) > 0)
|
|
732
|
+
return true;
|
|
733
|
+
return Array.from(this.pendingQueueConfirmations.values()).some((pending) => pending.info.threadId === threadId);
|
|
734
|
+
}
|
|
452
735
|
/**
|
|
453
736
|
* Bare /model: show the thread's current model on the active backend plus
|
|
454
737
|
* the available models. Listing source per backend (all verified live):
|
|
@@ -459,7 +742,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
459
742
|
*/
|
|
460
743
|
async handleModelList(messageId, threadId) {
|
|
461
744
|
const executorConfig = this.config.get('executor') ?? { type: 'auto' };
|
|
462
|
-
const key =
|
|
745
|
+
const key = this.threadPool.getBackendKey(threadId);
|
|
463
746
|
const thread = this.threadManager.getThread(threadId);
|
|
464
747
|
const current = thread?.models?.[key] ?? (key === 'claude' ? thread?.model : undefined);
|
|
465
748
|
const backendLabel = key === 'claude' ? 'Claude Code' : key === 'agy' ? 'AGY CLI' : 'Codex CLI';
|
|
@@ -553,7 +836,8 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
553
836
|
const lines = summaries.map(t => {
|
|
554
837
|
const icon = t.status === 'running' ? '🔄' : t.status === 'error' ? '❌' : '✅';
|
|
555
838
|
const current = t.id === callerThreadId ? ' ← (this thread)' : '';
|
|
556
|
-
|
|
839
|
+
const backend = t.backend ? ` [${t.backend}]` : '';
|
|
840
|
+
return `${icon} ${t.name}${backend}${current}`;
|
|
557
841
|
});
|
|
558
842
|
this.sendResponse(messageId, callerThreadId, {
|
|
559
843
|
success: true,
|
|
@@ -567,7 +851,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
567
851
|
const callerExecutor = this.threadPool.getExecutor(callerThread.id);
|
|
568
852
|
const cwd = callerExecutor.getCurrentWorkingDirectory();
|
|
569
853
|
try {
|
|
570
|
-
const newThread = await this.threadManager.createThread(name || this.generateThreadName(), cwd);
|
|
854
|
+
const newThread = await this.threadManager.createThread(name || this.generateThreadName(), cwd, this.threadPool.getBackendKey(callerThread.id));
|
|
571
855
|
// Use newThread.id so the router maps this card to the new thread,
|
|
572
856
|
// enabling the user to reply to this card to target the new thread.
|
|
573
857
|
this.sendResponse(messageId, newThread.id, {
|
|
@@ -876,18 +1160,43 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
876
1160
|
'/help', '/model', '/skills', '/usage', '/config', '/changelog',
|
|
877
1161
|
'/agents', '/permissions', '/hooks', '/credits',
|
|
878
1162
|
]);
|
|
1163
|
+
async listLocalSkills(cwd, backend) {
|
|
1164
|
+
const roots = backend === 'codex'
|
|
1165
|
+
? [(0, path_1.join)(cwd, '.agents', 'skills'), (0, path_1.join)((0, os_1.homedir)(), '.codex', 'skills'), (0, path_1.join)((0, os_1.homedir)(), '.codex', 'skills', '.system')]
|
|
1166
|
+
: [(0, path_1.join)(cwd, '.claude', 'skills'), (0, path_1.join)((0, os_1.homedir)(), '.claude', 'skills')];
|
|
1167
|
+
const skills = [];
|
|
1168
|
+
const seen = new Set();
|
|
1169
|
+
for (const root of roots) {
|
|
1170
|
+
let entries;
|
|
1171
|
+
try {
|
|
1172
|
+
entries = await (0, promises_1.readdir)(root, { withFileTypes: true });
|
|
1173
|
+
}
|
|
1174
|
+
catch {
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
for (const entry of entries) {
|
|
1178
|
+
if (!entry.isDirectory() || seen.has(entry.name))
|
|
1179
|
+
continue;
|
|
1180
|
+
const skillPath = (0, path_1.join)(root, entry.name, 'SKILL.md');
|
|
1181
|
+
try {
|
|
1182
|
+
const content = await (0, promises_1.readFile)(skillPath, 'utf8');
|
|
1183
|
+
const description = content.match(/^description:\s*(.+)$/mi)?.[1]?.trim();
|
|
1184
|
+
skills.push(description ? `${entry.name} — ${description}` : entry.name);
|
|
1185
|
+
seen.add(entry.name);
|
|
1186
|
+
}
|
|
1187
|
+
catch {
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return skills.sort((left, right) => left.localeCompare(right));
|
|
1193
|
+
}
|
|
879
1194
|
/**
|
|
880
|
-
* Which backend slash commands should be forwarded to
|
|
1195
|
+
* Which backend slash commands should be forwarded to for a thread.
|
|
881
1196
|
* 'auto'/claude-* always resolve to Claude; legacy 'gemini' means AGY.
|
|
882
1197
|
*/
|
|
883
|
-
resolveSlashBackend() {
|
|
884
|
-
|
|
885
|
-
const t = cfg.type;
|
|
886
|
-
if (t === 'agy' || t === 'gemini')
|
|
887
|
-
return 'agy';
|
|
888
|
-
if (t === 'codex')
|
|
889
|
-
return 'codex';
|
|
890
|
-
return 'claude';
|
|
1198
|
+
resolveSlashBackend(threadId) {
|
|
1199
|
+
return this.threadPool.getBackendKey(threadId);
|
|
891
1200
|
}
|
|
892
1201
|
/**
|
|
893
1202
|
* Execute passthrough slash command on the active backend's CLI.
|
|
@@ -901,7 +1210,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
901
1210
|
* implemented through app-server methods instead.
|
|
902
1211
|
*/
|
|
903
1212
|
async executeSlashCommand(messageId, threadId, command, executor) {
|
|
904
|
-
const backend = this.resolveSlashBackend();
|
|
1213
|
+
const backend = this.resolveSlashBackend(threadId);
|
|
905
1214
|
if (backend === 'codex') {
|
|
906
1215
|
this.sendResponse(messageId, threadId, {
|
|
907
1216
|
success: false,
|
|
@@ -1009,7 +1318,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
1009
1318
|
success: false,
|
|
1010
1319
|
error: `❌ Auto-compact failed: ${compactResult.error}\n\nUse /compact to try again, or /clear to start fresh.`,
|
|
1011
1320
|
});
|
|
1012
|
-
return;
|
|
1321
|
+
return false;
|
|
1013
1322
|
}
|
|
1014
1323
|
this.sendStreamChunk(messageId, threadId, '✅ Compaction done. Retrying your request...\n');
|
|
1015
1324
|
const retryResult = await executor.execute(content, {
|
|
@@ -1021,28 +1330,30 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
1021
1330
|
attachments,
|
|
1022
1331
|
});
|
|
1023
1332
|
this.sendResponse(messageId, threadId, { success: retryResult.success, error: retryResult.error, threads: this.threadPool.getSummaries() });
|
|
1024
|
-
return;
|
|
1333
|
+
return retryResult.success;
|
|
1025
1334
|
}
|
|
1026
1335
|
this.sendResponse(messageId, threadId, {
|
|
1027
1336
|
success: false,
|
|
1028
1337
|
error: '❌ Conversation history too long.\n\nUse /compact to compress it, or /clear to start fresh.',
|
|
1029
1338
|
});
|
|
1030
|
-
return;
|
|
1339
|
+
return false;
|
|
1031
1340
|
}
|
|
1032
1341
|
this.sendResponse(messageId, threadId, { success: result.success, error: result.error, threads: this.threadPool.getSummaries() });
|
|
1342
|
+
return result.success;
|
|
1033
1343
|
}
|
|
1034
1344
|
catch (error) {
|
|
1035
1345
|
this.sendResponse(messageId, threadId, {
|
|
1036
1346
|
success: false,
|
|
1037
1347
|
error: error instanceof Error ? error.message : 'Execution error',
|
|
1038
1348
|
});
|
|
1349
|
+
return false;
|
|
1039
1350
|
}
|
|
1040
1351
|
}
|
|
1041
1352
|
async clearInvalidCodexModel(threadId, executor, error) {
|
|
1042
1353
|
if (!error || !this.isUnavailableCodexModelError(error))
|
|
1043
1354
|
return false;
|
|
1044
1355
|
const executorConfig = this.config.get('executor') ?? { type: 'auto' };
|
|
1045
|
-
if (
|
|
1356
|
+
if (this.threadPool.getBackendKey(threadId) !== 'codex')
|
|
1046
1357
|
return false;
|
|
1047
1358
|
const thread = this.threadManager.getThread(threadId);
|
|
1048
1359
|
if (!thread?.models?.codex)
|
|
@@ -1058,6 +1369,9 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
1058
1369
|
|| /unknown Codex model/i.test(error);
|
|
1059
1370
|
}
|
|
1060
1371
|
// ── Outgoing message helpers ──────────────────────────────────────────────
|
|
1372
|
+
getMessageOpenId(messageId) {
|
|
1373
|
+
return this.messageOpenIds.get(messageId) ?? this.currentOpenId;
|
|
1374
|
+
}
|
|
1061
1375
|
sendStreamChunk(messageId, threadId, chunk) {
|
|
1062
1376
|
try {
|
|
1063
1377
|
this.wsClient.send({
|
|
@@ -1065,7 +1379,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
1065
1379
|
messageId,
|
|
1066
1380
|
chunk,
|
|
1067
1381
|
streamType: 'text',
|
|
1068
|
-
openId: this.
|
|
1382
|
+
openId: this.getMessageOpenId(messageId),
|
|
1069
1383
|
threadId,
|
|
1070
1384
|
timestamp: Date.now(),
|
|
1071
1385
|
});
|
|
@@ -1081,7 +1395,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
1081
1395
|
messageId,
|
|
1082
1396
|
streamType: 'tool_use',
|
|
1083
1397
|
toolUse,
|
|
1084
|
-
openId: this.
|
|
1398
|
+
openId: this.getMessageOpenId(messageId),
|
|
1085
1399
|
threadId,
|
|
1086
1400
|
timestamp: Date.now(),
|
|
1087
1401
|
});
|
|
@@ -1097,7 +1411,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
1097
1411
|
messageId,
|
|
1098
1412
|
streamType: 'tool_result',
|
|
1099
1413
|
toolResult,
|
|
1100
|
-
openId: this.
|
|
1414
|
+
openId: this.getMessageOpenId(messageId),
|
|
1101
1415
|
threadId,
|
|
1102
1416
|
timestamp: Date.now(),
|
|
1103
1417
|
});
|
|
@@ -1112,7 +1426,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
1112
1426
|
type: 'stream',
|
|
1113
1427
|
messageId,
|
|
1114
1428
|
streamType: 'redacted_thinking',
|
|
1115
|
-
openId: this.
|
|
1429
|
+
openId: this.getMessageOpenId(messageId),
|
|
1116
1430
|
threadId,
|
|
1117
1431
|
timestamp: Date.now(),
|
|
1118
1432
|
});
|
|
@@ -1128,7 +1442,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
1128
1442
|
messageId,
|
|
1129
1443
|
streamType: 'plan_mode',
|
|
1130
1444
|
planContent,
|
|
1131
|
-
openId: this.
|
|
1445
|
+
openId: this.getMessageOpenId(messageId),
|
|
1132
1446
|
threadId,
|
|
1133
1447
|
timestamp: Date.now(),
|
|
1134
1448
|
});
|
|
@@ -1143,7 +1457,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
1143
1457
|
type: 'structured',
|
|
1144
1458
|
messageId,
|
|
1145
1459
|
structuredContent,
|
|
1146
|
-
openId: this.
|
|
1460
|
+
openId: this.getMessageOpenId(messageId),
|
|
1147
1461
|
threadId,
|
|
1148
1462
|
timestamp: Date.now(),
|
|
1149
1463
|
});
|
|
@@ -1171,12 +1485,14 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
1171
1485
|
output: result.output,
|
|
1172
1486
|
error: result.error,
|
|
1173
1487
|
sessionAbbr: result.sessionAbbr,
|
|
1174
|
-
openId: this.
|
|
1488
|
+
openId: this.getMessageOpenId(messageId),
|
|
1175
1489
|
threadId,
|
|
1176
1490
|
threads: result.threads,
|
|
1491
|
+
queueConfirmation: result.queueConfirmation,
|
|
1177
1492
|
cwd,
|
|
1178
1493
|
timestamp: Date.now(),
|
|
1179
1494
|
});
|
|
1495
|
+
this.messageOpenIds.delete(messageId);
|
|
1180
1496
|
}
|
|
1181
1497
|
catch (error) {
|
|
1182
1498
|
console.error('Failed to send response:', error);
|
|
@@ -1203,6 +1519,7 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
1203
1519
|
async handleBackendCommand(messageId, threadId, trimmed) {
|
|
1204
1520
|
const parts = trimmed.split(/\s+/);
|
|
1205
1521
|
const arg = parts[1];
|
|
1522
|
+
const isThreadOverride = parts[2] === '@';
|
|
1206
1523
|
const currentConfig = this.config.get('executor') ?? { type: 'auto' };
|
|
1207
1524
|
const currentType = currentConfig.type;
|
|
1208
1525
|
const backends = await this.detectBackends();
|
|
@@ -1224,9 +1541,30 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
1224
1541
|
const active = b.id === currentType || isClaudeActive || isAgyActive ? ' ★ (active)' : '';
|
|
1225
1542
|
return `${i + 1}. ${b.label}${active}`;
|
|
1226
1543
|
});
|
|
1544
|
+
const currentBackend = this.threadPool.getBackendKey(threadId);
|
|
1545
|
+
lines.push(`\nCurrent thread backend: ${currentBackend}${this.threadManager.getThread(threadId)?.backend ? ' (override)' : ' (global)'}`);
|
|
1227
1546
|
this.sendResponse(messageId, threadId, {
|
|
1228
1547
|
success: true,
|
|
1229
|
-
output: `🤖 Available AI backends:\n${lines.join('\n')}\n\nSwitch with: /backend <index>
|
|
1548
|
+
output: `🤖 Available AI backends:\n${lines.join('\n')}\n\nSwitch all threads with: /backend <index>\nSwitch this thread with: /backend <index> @\nReset this thread: /backend default @`,
|
|
1549
|
+
threads: this.threadPool.getSummaries(),
|
|
1550
|
+
});
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (arg === 'default' && isThreadOverride) {
|
|
1554
|
+
if (this.threadPool.isThreadBusy(threadId)) {
|
|
1555
|
+
this.sendResponse(messageId, threadId, {
|
|
1556
|
+
success: false,
|
|
1557
|
+
error: 'Cannot reset backend while this thread is running. Send /abort first.',
|
|
1558
|
+
});
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
await this.threadPool.destroyThread(threadId, { deleteData: false });
|
|
1562
|
+
await this.threadManager.updateThread(threadId, { backend: undefined });
|
|
1563
|
+
const clearedCount = this.clearThreadQueue(threadId);
|
|
1564
|
+
this.sendResponse(messageId, threadId, {
|
|
1565
|
+
success: true,
|
|
1566
|
+
output: `✅ Thread backend reset to the global backend (${(0, config_1.backendKeyOf)(currentType)}).${clearedCount ? `\n🗑️ Cleared ${clearedCount} queued message${clearedCount === 1 ? '' : 's'}.` : ''}`,
|
|
1567
|
+
threads: this.threadPool.getSummaries(),
|
|
1230
1568
|
});
|
|
1231
1569
|
return;
|
|
1232
1570
|
}
|
|
@@ -1246,11 +1584,39 @@ You can also use natural language commands to control Claude Code CLI.`,
|
|
|
1246
1584
|
return;
|
|
1247
1585
|
}
|
|
1248
1586
|
const newConfig = { ...currentConfig, type: target.id };
|
|
1587
|
+
if (isThreadOverride) {
|
|
1588
|
+
try {
|
|
1589
|
+
await this.threadPool.switchThreadBackend(threadId, (0, config_1.backendKeyOf)(target.id));
|
|
1590
|
+
const clearedCount = this.clearThreadQueue(threadId);
|
|
1591
|
+
this.sendResponse(messageId, threadId, {
|
|
1592
|
+
success: true,
|
|
1593
|
+
output: `✅ This thread switched to: ${target.label}${clearedCount ? `\n🗑️ Cleared ${clearedCount} queued message${clearedCount === 1 ? '' : 's'}.` : ''}\n\nUse /backend ${installed.indexOf(target) + 1} to switch all threads, or /backend default @ to follow the global backend again.`,
|
|
1594
|
+
threads: this.threadPool.getSummaries(),
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
catch (error) {
|
|
1598
|
+
this.sendResponse(messageId, threadId, {
|
|
1599
|
+
success: false,
|
|
1600
|
+
error: error instanceof Error ? error.message : 'Failed to switch thread backend',
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
if (this.threadPool.getSummaries().some((thread) => thread.status === 'running')) {
|
|
1606
|
+
this.sendResponse(messageId, threadId, {
|
|
1607
|
+
success: false,
|
|
1608
|
+
error: 'Cannot switch all backends while a thread is running. Send /abort first.',
|
|
1609
|
+
});
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1249
1612
|
await this.config.set('executor', newConfig);
|
|
1250
1613
|
await this.threadPool.switchBackend(newConfig);
|
|
1614
|
+
await this.threadManager.clearBackendOverrides();
|
|
1615
|
+
const clearedCount = this.clearAllQueues();
|
|
1251
1616
|
this.sendResponse(messageId, threadId, {
|
|
1252
1617
|
success: true,
|
|
1253
|
-
output: `✅ Backend switched to: ${target.label}\n\nAll threads will use the new backend for future commands. Conversations on the previous backend are preserved — switch back to resume them.`,
|
|
1618
|
+
output: `✅ Backend switched to: ${target.label}${clearedCount ? `\n🗑️ Cleared ${clearedCount} queued message${clearedCount === 1 ? '' : 's'}.` : ''}\n\nAll threads will use the new backend for future commands. Conversations on the previous backend are preserved — switch back to resume them.`,
|
|
1619
|
+
threads: this.threadPool.getSummaries(),
|
|
1254
1620
|
});
|
|
1255
1621
|
}
|
|
1256
1622
|
/**
|