agentp 0.11.11 → 0.12.0
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 +14 -7
- package/bin/tgagentp +299 -258
- package/lib/ocmux.js +27 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -231,21 +231,28 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
|
|
|
231
231
|
### Slash commands
|
|
232
232
|
|
|
233
233
|
| Command | Action |
|
|
234
|
-
|
|
234
|
+
|---|---|---|
|
|
235
235
|
| `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`, `allow`, `think`, `record`, `queue`) |
|
|
236
236
|
| `/servers` | List all ocmux-served projects (▶ active, 🔌 disconnected, 💀 dead) |
|
|
237
|
-
| `/
|
|
237
|
+
| `/server <name>` | Switch active server; matches by full path, basename, or substring |
|
|
238
|
+
| `/server --force <name>` | Take over a server from another chat |
|
|
238
239
|
| `/resurrect [path]` | Restart a crashed server from its `.ocmux.json`; accepts optional directory path |
|
|
240
|
+
| `/sessions` | List sessions (numbered, newest first) |
|
|
241
|
+
| `/session <name-or-number>` | Switch to a session by name or position |
|
|
242
|
+
| `/session new [name]` | Create a new session |
|
|
243
|
+
| `/session rename <name>` | Rename the active session |
|
|
244
|
+
| `/agents` | List available agents/models |
|
|
245
|
+
| `/agent <name>` | Switch the active agent for subsequent messages (synced to server via API) |
|
|
246
|
+
| `/models` | List providers and models (▶ marks current) |
|
|
247
|
+
| `/model <providerID/modelID>` | Switch session model |
|
|
239
248
|
| `/allow` | Approve a permission request once |
|
|
240
249
|
| `/reject` | Deny a permission request |
|
|
241
250
|
| `/always` | Approve and remember for the session |
|
|
242
251
|
| `/answer <number>` | Respond to a question asked by the AI (structured multiple-choice) |
|
|
243
252
|
| `/markdown` | Send the original markdown of the last response as a `.md` file; reply to a message to get that specific response |
|
|
244
|
-
| `/agents list` | List available agents/models |
|
|
245
|
-
| `/agents switch <name>` | Switch the active agent for subsequent messages (synced to server via API) |
|
|
246
253
|
| `/shutdown [force\|clear]` | (requires `--dev`) Stop tgagentp; `clear` also wipes saved connections |
|
|
247
254
|
|
|
248
|
-
> **Note on model/agent switching:** Changing the model in the OpenCode TUI's prompt dropdown is a **local UI action** — it only takes effect server-side after a message is sent *through the TUI*. If you change the model in the TUI and then send a prompt via `agentp` or Telegram, the old model will still be used. Use `/
|
|
255
|
+
> **Note on model/agent switching:** Changing the model in the OpenCode TUI's prompt dropdown is a **local UI action** — it only takes effect server-side after a message is sent *through the TUI*. If you change the model in the TUI and then send a prompt via `agentp` or Telegram, the old model will still be used. Use `/agent <name>` or `/model <providerID/modelID>` from Telegram (or `tgagentp`) to change models — this explicitly calls the API and syncs correctly.
|
|
249
256
|
|
|
250
257
|
#### TUI Command Passthrough
|
|
251
258
|
|
|
@@ -255,7 +262,7 @@ Messages starting with `//` are forwarded to the OpenCode TUI as raw keystrokes
|
|
|
255
262
|
|
|
256
263
|
### Chat-server ownership
|
|
257
264
|
|
|
258
|
-
Each server can be owned by at most one chat at a time. New chats start disconnected. Use `/
|
|
265
|
+
Each server can be owned by at most one chat at a time. New chats start disconnected. Use `/server <name>` to connect; `--force` takes over and notifies the previous owner. Connections are persisted to `/tmp/tgagentp-connections.json` and restored automatically on restart (server URL is re-discovered from `.ocmux.json`).
|
|
259
266
|
|
|
260
267
|
### Per-server state
|
|
261
268
|
|
|
@@ -287,7 +294,7 @@ tgagentp starts a tiny HTTP server on `127.0.0.1` that accepts `POST /send` requ
|
|
|
287
294
|
- Port is written to `/tmp/tgagentp-port` for agentp discovery.
|
|
288
295
|
- Authentication reuses `OPENCODE_SERVER_PASSWORD`.
|
|
289
296
|
- Messages for the owning chat's active server are delivered immediately.
|
|
290
|
-
- Messages for non-active servers are queued per-server with debounced notifications (configurable via `TGAGENTP_DEBOUNCE_MS`); delivered on `/
|
|
297
|
+
- Messages for non-active servers are queued per-server with debounced notifications (configurable via `TGAGENTP_DEBOUNCE_MS`); delivered on `/server <name>`.
|
|
291
298
|
- Server health detection pre-sends: if a server is unreachable, messages are auto-queued and delivered when it comes back. `/flush` clears all queues.
|
|
292
299
|
- When [/record](#tgagentp) is active, the gateway response includes the recorded conversation buffer. `agentp --qa` prepends this buffer (with rulers) to its stdout so the full Telegram context is available to OpenCode. Use `agentp --qa --flush` to flush the buffer without prepending.
|
|
293
300
|
|
package/bin/tgagentp
CHANGED
|
@@ -264,46 +264,80 @@ async function cmdHelp(token, chatId, args, chatState) {
|
|
|
264
264
|
const topic = args[0].toLowerCase();
|
|
265
265
|
if (topic === 'servers') {
|
|
266
266
|
await sendLongMessage(token, chatState,
|
|
267
|
-
'`/servers` ─ list
|
|
268
|
-
'
|
|
269
|
-
'
|
|
270
|
-
'
|
|
271
|
-
'
|
|
267
|
+
'`/servers` ─ list running servers\n' +
|
|
268
|
+
' • Shows alive and dead servers\n' +
|
|
269
|
+
' • Your server marked with ▶\n\n' +
|
|
270
|
+
'`/server <name>` ─ switch to a server by directory path or basename\n' +
|
|
271
|
+
'`/server --force <name>` ─ take over a server from another chat');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (topic === 'agents') {
|
|
275
|
+
await sendLongMessage(token, chatState,
|
|
276
|
+
'`/agents` ─ list primary agents\n' +
|
|
277
|
+
' • Current agent marked with ▶\n' +
|
|
278
|
+
' • Shows model and description\n\n' +
|
|
279
|
+
'`/agent <name>` ─ switch to an agent');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (topic === 'sessions') {
|
|
283
|
+
await sendLongMessage(token, chatState,
|
|
284
|
+
'`/sessions` ─ list sessions\n' +
|
|
285
|
+
' • Primary-agent sessions shown by default (includes subagent sessions to maintain context)\n' +
|
|
286
|
+
' • Active session marked with ▶\n' +
|
|
287
|
+
' • Grouped by recency: Today, this week, older months\n\n' +
|
|
288
|
+
'`/session <name-or-number>` ─ switch to a session by name or position\n' +
|
|
289
|
+
'`/session new [name]` ─ create a new session\n' +
|
|
290
|
+
'`/session rename <name>` ─ rename the active session');
|
|
272
291
|
return;
|
|
273
292
|
}
|
|
274
293
|
if (topic === 'sessions') {
|
|
275
294
|
await sendLongMessage(token, chatState,
|
|
276
295
|
'`/sessions` ─ list active sessions (numbered, newest first, max 50)\n' +
|
|
277
|
-
'`/
|
|
278
|
-
'`/
|
|
279
|
-
'`/
|
|
296
|
+
'`/session <name-or-number>` ─ switch to a session by name or position\n' +
|
|
297
|
+
'`/session new [name]` ─ create a new session\n' +
|
|
298
|
+
'`/session rename <name>` ─ rename the active session\n\n' +
|
|
280
299
|
'Switch by number (from the list) or by case-insensitive partial name match.\n' +
|
|
281
300
|
'Use `/sessions` to find session numbers.');
|
|
282
301
|
return;
|
|
283
302
|
}
|
|
284
|
-
if (topic === '
|
|
303
|
+
if (topic === 'servers') {
|
|
285
304
|
await sendLongMessage(token, chatState,
|
|
286
|
-
'`/
|
|
287
|
-
'
|
|
288
|
-
'
|
|
289
|
-
'
|
|
290
|
-
'
|
|
305
|
+
'`/servers` ─ list running servers\n' +
|
|
306
|
+
' • Shows alive and dead servers\n' +
|
|
307
|
+
' • Your server marked with ▶\n\n' +
|
|
308
|
+
'`/server <name>` ─ switch to a server by directory path or basename\n' +
|
|
309
|
+
'`/server --force <name>` ─ take over a server from another chat');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (topic === 'server') {
|
|
313
|
+
await sendLongMessage(token, chatState,
|
|
314
|
+
'`/server <name>` ─ switch to a server by directory path or basename.\n' +
|
|
315
|
+
'`/server --force <name>` ─ take over a server from another chat.\n\n' +
|
|
316
|
+
'Matches by full directory path, basename, or case-insensitive substring.\n' +
|
|
317
|
+
'Use `/servers` to see available servers.');
|
|
291
318
|
return;
|
|
292
319
|
}
|
|
293
320
|
if (topic === 'agents') {
|
|
294
321
|
await sendLongMessage(token, chatState,
|
|
295
322
|
'`/agents` ─ list primary agents (with ▶ marker for the active one).\n' +
|
|
296
|
-
'`/
|
|
297
|
-
'Agent switching requires an active session (`/
|
|
323
|
+
'`/agent <name>` ─ switch the active agent. Subsequent messages will use this agent.\n\n' +
|
|
324
|
+
'Agent switching requires an active session (`/session <name>` or `/session new`).');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (topic === 'agent') {
|
|
328
|
+
await sendLongMessage(token, chatState,
|
|
329
|
+
'`/agent <name>` ─ switch the active agent.\n\n' +
|
|
330
|
+
'Matches by exact name first, then case-insensitive substring.\n' +
|
|
331
|
+
'The agent is synced to the server session via API.\n' +
|
|
332
|
+
'Use `/agents` to list available agents.');
|
|
298
333
|
return;
|
|
299
334
|
}
|
|
300
335
|
if (topic === 'session') {
|
|
301
336
|
await sendLongMessage(token, chatState,
|
|
302
|
-
'`/session
|
|
303
|
-
'`/
|
|
304
|
-
'`/
|
|
305
|
-
'`/sessions
|
|
306
|
-
'`/sessions rename <name>` ─ rename the active session');
|
|
337
|
+
'`/session <name-or-number>` ─ switch to a session by name or position\n' +
|
|
338
|
+
'`/session new [name]` ─ create a new session\n' +
|
|
339
|
+
'`/session rename <name>` ─ rename the active session\n\n' +
|
|
340
|
+
'Use `/sessions` to list available sessions with numbers.');
|
|
307
341
|
return;
|
|
308
342
|
}
|
|
309
343
|
if (topic === 'models') {
|
|
@@ -390,7 +424,7 @@ async function cmdHelp(token, chatId, args, chatState) {
|
|
|
390
424
|
if (topic === 'disconnect') {
|
|
391
425
|
await sendLongMessage(token, chatState,
|
|
392
426
|
'`/disconnect` ─ disconnect this chat from its server\n\n' +
|
|
393
|
-
'Clears the server connection for this chat. Use `/
|
|
427
|
+
'Clears the server connection for this chat. Use `/server <name>` to reconnect.');
|
|
394
428
|
return;
|
|
395
429
|
}
|
|
396
430
|
if (topic === 'status') {
|
|
@@ -453,7 +487,7 @@ async function cmdHelp(token, chatId, args, chatState) {
|
|
|
453
487
|
return;
|
|
454
488
|
}
|
|
455
489
|
await sendLongMessage(token, chatState,
|
|
456
|
-
'Unknown topic. Try `/help servers`, `/help sessions`, `/help agents`, `/help models`, `/help allow`, `/help think`, `/help record`, `/help queue`, `/help flush`, `/help note`, `/help comment`, `/help disconnect`, `/help serve`, `/help new`, `/help markdown`, or just `/help`.');
|
|
490
|
+
'Unknown topic. Try `/help servers`, `/help server`, `/help sessions`, `/help session`, `/help agents`, `/help agent`, `/help models`, `/help model`, `/help allow`, `/help think`, `/help record`, `/help queue`, `/help flush`, `/help note`, `/help comment`, `/help disconnect`, `/help serve`, `/help new`, `/help markdown`, or just `/help`.');
|
|
457
491
|
return;
|
|
458
492
|
}
|
|
459
493
|
await sendLongMessage(token, chatState,
|
|
@@ -462,16 +496,16 @@ async function cmdHelp(token, chatId, args, chatState) {
|
|
|
462
496
|
'`/status` ─ show current server and session\n' +
|
|
463
497
|
'`/cancel` ─ cancel the running prompt\n' +
|
|
464
498
|
'`/servers` ─ list running opencode servers\n' +
|
|
465
|
-
'`/
|
|
466
|
-
'`/force
|
|
467
|
-
'`/sessions` ─ list
|
|
468
|
-
'`/
|
|
469
|
-
'`/
|
|
470
|
-
'`/
|
|
471
|
-
'`/session` ─ alias for `/sessions`\n' +
|
|
499
|
+
'`/server <name>` ─ switch to a different server\n' +
|
|
500
|
+
'`/server --force <name>` ─ take over a server from another chat\n' +
|
|
501
|
+
'`/sessions` ─ list sessions\n' +
|
|
502
|
+
'`/session <name-or-number>` ─ switch to a session\n' +
|
|
503
|
+
'`/session new [name]` ─ create a new session\n' +
|
|
504
|
+
'`/session rename <name>` ─ rename the active session\n' +
|
|
472
505
|
'`/agents` ─ list available agents\n' +
|
|
473
|
-
'`/
|
|
506
|
+
'`/agent <name>` ─ switch active agent\n' +
|
|
474
507
|
'`/models` ─ list providers and models\n' +
|
|
508
|
+
'`/model <provider/model>` ─ switch session model\n' +
|
|
475
509
|
'`/think` ─ toggle thinking message forwarding\n' +
|
|
476
510
|
'`/record` ─ record Telegram conversation for agentp context\n' +
|
|
477
511
|
'`/queue <message>` ─ queue a message when server is busy\n' +
|
|
@@ -489,90 +523,18 @@ async function cmdHelp(token, chatId, args, chatState) {
|
|
|
489
523
|
'Everything else is forwarded to the active server as `[Telegram] <text>`.');
|
|
490
524
|
}
|
|
491
525
|
|
|
492
|
-
async function cmdForceSwitch(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
493
|
-
return cmdServers(token, chatId, ['switch', '--force', ...args], chatState, setServerForChat, setActiveSessionForChat);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
526
|
async function cmdServers(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
497
|
-
|
|
498
|
-
if (args.length > 0
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
'Usage: `/servers switch <name>`\n' +
|
|
505
|
-
'To force-switch (take over a server from another chat), use `/force-switch <name>`.\n' +
|
|
506
|
-
'`<name>` can be a full directory path or a basename.');
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
const name = switchArgs.join(' ');
|
|
510
|
-
const servers = listServers();
|
|
511
|
-
if (servers.length === 0) {
|
|
512
|
-
await sendLongMessage(token, chatState, 'No servers running.');
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// Phase 1: case-insensitive exact match on dir or basename
|
|
517
|
-
let matches = servers.filter(s => s.dir.toLowerCase() === name.toLowerCase());
|
|
518
|
-
if (matches.length === 0) {
|
|
519
|
-
matches = servers.filter(s => path.basename(s.dir).toLowerCase() === name.toLowerCase());
|
|
520
|
-
}
|
|
521
|
-
// Fallback: case-insensitive substring match
|
|
522
|
-
if (matches.length === 0) {
|
|
523
|
-
const lowerName = name.toLowerCase();
|
|
524
|
-
matches = servers.filter(s => s.dir.toLowerCase().includes(lowerName) || path.basename(s.dir).toLowerCase().includes(lowerName));
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (matches.length === 1) {
|
|
528
|
-
// Phase 1 found a single match — use it directly
|
|
529
|
-
} else if (matches.length === 0) {
|
|
530
|
-
await sendLongMessage(token, chatState, `No server found matching "${name}".`);
|
|
531
|
-
return;
|
|
532
|
-
} else {
|
|
533
|
-
// Phase 2: case-sensitive exact match to disambiguate
|
|
534
|
-
let exact = servers.filter(s => s.dir === name);
|
|
535
|
-
if (exact.length === 0) {
|
|
536
|
-
exact = servers.filter(s => path.basename(s.dir) === name);
|
|
537
|
-
}
|
|
538
|
-
if (exact.length === 1) {
|
|
539
|
-
matches = exact;
|
|
540
|
-
} else {
|
|
541
|
-
const list = matches.map(s => ` ${s.url} → ${s.dir}`).join('\n');
|
|
542
|
-
await sendLongMessage(token, chatState,
|
|
543
|
-
`Multiple matches for "${name}":\n${list}\n\nPlease be more specific.`);
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
const target = matches[0];
|
|
548
|
-
const existing = serverOwners[target.url];
|
|
549
|
-
if (existing && existing.chatId !== chatState.chatId && !force) {
|
|
550
|
-
await sendLongMessage(token, chatState,
|
|
551
|
-
`⚠️ Server \`${target.url}\` is already connected to another chat.\n\n` +
|
|
552
|
-
`Use \`/servers force-switch ${name}\` to take over (the other chat will be disconnected).`);
|
|
553
|
-
return;
|
|
527
|
+
// Backward compatibility: redirect old subcommands to singular commands
|
|
528
|
+
if (args.length > 0) {
|
|
529
|
+
const sub = args[0].toLowerCase();
|
|
530
|
+
if (sub === 'switch') {
|
|
531
|
+
const rest = args.slice(1).filter(a => a !== '--force');
|
|
532
|
+
const force = args.includes('--force');
|
|
533
|
+
return cmdServer(token, chatId, force ? ['--force', ...rest] : rest, chatState, setServerForChat, setActiveSessionForChat);
|
|
554
534
|
}
|
|
555
|
-
if (
|
|
556
|
-
|
|
557
|
-
}
|
|
558
|
-
if (force && existing && existing.chatId !== chatState.chatId) {
|
|
559
|
-
// Reset the old owner's server and notify them
|
|
560
|
-
const oldState = getChatState(existing.chatId, existing.threadId);
|
|
561
|
-
oldState.serverBase = null;
|
|
562
|
-
sendLongMessage(token, oldState,
|
|
563
|
-
`⚠️ Server \`${target.url}\` has been taken over by another chat.\n\n` +
|
|
564
|
-
`Use \`/servers\` to see available servers and reconnect.`).catch(() => {});
|
|
565
|
-
}
|
|
566
|
-
const pending = await setServerForChat(chatState, target.url);
|
|
567
|
-
activateServer(target.dir, target.index, target.url);
|
|
568
|
-
let msg = `Switched to server: ${target.url} → ${target.dir}`;
|
|
569
|
-
if (pending && pending.trim()) {
|
|
570
|
-
await sendLongMessage(token, chatState, msg);
|
|
571
|
-
await sendLongMessage(token, chatState, pending.trim());
|
|
572
|
-
} else {
|
|
573
|
-
await sendLongMessage(token, chatState, msg);
|
|
535
|
+
if (sub === 'force-switch') {
|
|
536
|
+
return cmdServer(token, chatId, ['--force', ...args.slice(1)], chatState, setServerForChat, setActiveSessionForChat);
|
|
574
537
|
}
|
|
575
|
-
return;
|
|
576
538
|
}
|
|
577
539
|
|
|
578
540
|
// Default: list servers (alive + dead)
|
|
@@ -650,106 +612,19 @@ async function discoverActiveSession(server) {
|
|
|
650
612
|
|
|
651
613
|
async function cmdSessions(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
652
614
|
const server = chatState.serverBase;
|
|
615
|
+
// Backward compatibility: redirect old subcommands to singular commands
|
|
653
616
|
if (args.length > 0) {
|
|
654
617
|
const sub = args[0].toLowerCase();
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
const title = args.slice(1).join(' ') || undefined;
|
|
658
|
-
try {
|
|
659
|
-
const session = await createSession(server, title);
|
|
660
|
-
const name = session.title || session.slug || session.id;
|
|
661
|
-
setActiveSessionForChat(chatState, session.id, name);
|
|
662
|
-
selectSession(server, session.id);
|
|
663
|
-
await sendLongMessage(token, chatState,
|
|
664
|
-
`Created session: \`${name}\`\nIt is now the active session.`);
|
|
665
|
-
} catch (err) {
|
|
666
|
-
await sendLongMessage(token, chatState, `Error creating session: ${err.message}`);
|
|
667
|
-
}
|
|
668
|
-
return;
|
|
618
|
+
if (sub === 'switch' || sub === 'new' || sub === 'rename') {
|
|
619
|
+
return cmdSession(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat);
|
|
669
620
|
}
|
|
670
|
-
|
|
671
|
-
if (sub === 'rename') {
|
|
672
|
-
if (args.length < 2) {
|
|
673
|
-
await sendLongMessage(token, chatState, 'Usage: `/sessions rename <name>`');
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
const st = getState(server);
|
|
677
|
-
if (!st.activeSessionId) {
|
|
678
|
-
await sendLongMessage(token, chatState, 'No active session. Use `/sessions switch` or `/sessions new` first.');
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
const newTitle = args.slice(1).join(' ');
|
|
682
|
-
try {
|
|
683
|
-
const session = await updateSession(server, st.activeSessionId, newTitle);
|
|
684
|
-
const name = session.title || session.slug || session.id;
|
|
685
|
-
await sendLongMessage(token, chatState,
|
|
686
|
-
`Renamed session to: \`${name}\``);
|
|
687
|
-
} catch (err) {
|
|
688
|
-
await sendLongMessage(token, chatState, `Error renaming session: ${err.message}`);
|
|
689
|
-
}
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
if (sub === 'switch') {
|
|
694
|
-
if (args.length < 2) {
|
|
695
|
-
await sendLongMessage(token, chatState,
|
|
696
|
-
'Usage: `/sessions switch <number-or-name>`\n' +
|
|
697
|
-
'Use `/sessions` to see numbered list. You can switch by number or by name (case-insensitive partial match).');
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
const query = args.slice(1).join(' ');
|
|
701
|
-
try {
|
|
702
|
-
const projectDir = serverDir(server);
|
|
703
|
-
const sessions = await listSessions(server, projectDir);
|
|
704
|
-
const activeId = getState(server).activeSessionId;
|
|
705
|
-
const filtered = sessions.filter(s =>
|
|
706
|
-
(s.agent || s.directory || s.id === activeId) && (!projectDir || s.directory === projectDir));
|
|
707
|
-
const sorted = sortedSessions(filtered, 50);
|
|
708
|
-
|
|
709
|
-
// Try by position number first
|
|
710
|
-
const num = parseInt(query, 10);
|
|
711
|
-
if (/^\d+$/.test(query) && num >= 1 && num <= sorted.length) {
|
|
712
|
-
const target = sorted[num - 1];
|
|
713
|
-
setActiveSessionForChat(chatState, target.id, sessionName(target));
|
|
714
|
-
selectSession(server, target.id);
|
|
715
|
-
await sendLongMessage(token, chatState,
|
|
716
|
-
`Switched to session #${num}: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// Try by name (case-insensitive partial match)
|
|
721
|
-
const q = query.toLowerCase();
|
|
722
|
-
const matches = sorted.filter(s => sessionName(s).toLowerCase().includes(q));
|
|
723
|
-
if (matches.length === 0) {
|
|
724
|
-
await sendLongMessage(token, chatState,
|
|
725
|
-
`No session matching "${query}". Use /sessions to list active sessions.`);
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
if (matches.length > 1) {
|
|
729
|
-
const list = matches.map(s => ` \`${sessionName(s)}\``).join('\n');
|
|
730
|
-
await sendLongMessage(token, chatState,
|
|
731
|
-
`Multiple matches for "${query}":\n${list}\n\nPlease be more specific or use a number.`);
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
const target = matches[0];
|
|
735
|
-
setActiveSessionForChat(chatState, target.id);
|
|
736
|
-
selectSession(server, target.id);
|
|
737
|
-
await sendLongMessage(token, chatState,
|
|
738
|
-
`Switched to session: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
|
|
739
|
-
} catch (err) {
|
|
740
|
-
await sendLongMessage(token, chatState, `Error switching session: ${err.message}`);
|
|
741
|
-
}
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// Unknown subcommand
|
|
746
621
|
await sendLongMessage(token, chatState,
|
|
747
622
|
`Unknown subcommand: /sessions ${sub}\n\n` +
|
|
748
623
|
'Usage:\n' +
|
|
749
624
|
' `/sessions` ─ list sessions\n' +
|
|
750
|
-
' `/
|
|
751
|
-
' `/
|
|
752
|
-
' `/
|
|
625
|
+
' `/session <name-or-number>` ─ switch to a session\n' +
|
|
626
|
+
' `/session new [name]` ─ create a new session\n' +
|
|
627
|
+
' `/session rename <name>` ─ rename the active session');
|
|
753
628
|
return;
|
|
754
629
|
}
|
|
755
630
|
|
|
@@ -823,48 +698,9 @@ async function cmdSessions(token, chatId, args, chatState, setServerForChat, set
|
|
|
823
698
|
|
|
824
699
|
async function cmdAgents(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
825
700
|
const server = chatState.serverBase;
|
|
701
|
+
// Backward compatibility: redirect /agents switch to /agent
|
|
826
702
|
if (args.length > 0 && args[0].toLowerCase() === 'switch') {
|
|
827
|
-
|
|
828
|
-
await sendLongMessage(token, chatState,
|
|
829
|
-
'Usage: `/agents switch <name>`\n' +
|
|
830
|
-
'Use `/agents` to list available agents.');
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
const name = args.slice(1).join(' ');
|
|
834
|
-
try {
|
|
835
|
-
const agents = await listAgents(server);
|
|
836
|
-
if (!agents || agents.length === 0) {
|
|
837
|
-
await sendLongMessage(token, chatState, 'No agents available.');
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
let matches = agents.filter(a => a.name === name);
|
|
841
|
-
if (matches.length === 0) {
|
|
842
|
-
matches = agents.filter(a => a.name.toLowerCase().includes(name.toLowerCase()));
|
|
843
|
-
}
|
|
844
|
-
if (matches.length === 0) {
|
|
845
|
-
await sendLongMessage(token, chatState, `No agent matching "${name}".`);
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
if (matches.length > 1) {
|
|
849
|
-
const list = matches.map(a => ` \`${a.name}\``).join('\n');
|
|
850
|
-
await sendLongMessage(token, chatState,
|
|
851
|
-
`Multiple matches for "${name}":\n${list}\n\nPlease be more specific.`);
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
const target = matches[0];
|
|
855
|
-
getState(server).activeAgentName = target.name;
|
|
856
|
-
const st = getState(server);
|
|
857
|
-
if (st.activeSessionId) {
|
|
858
|
-
updateSession(server, st.activeSessionId, undefined, target.name);
|
|
859
|
-
selectSession(server, st.activeSessionId);
|
|
860
|
-
}
|
|
861
|
-
const model = target.model ? ` ─ \`${target.model.providerID}/${target.model.modelID}\`` : '';
|
|
862
|
-
await sendLongMessage(token, chatState,
|
|
863
|
-
`Switched to agent: **${target.name}**${model}\nSubsequent messages will use this agent.`);
|
|
864
|
-
} catch (err) {
|
|
865
|
-
await sendLongMessage(token, chatState, `Error switching agent: ${err.message}`);
|
|
866
|
-
}
|
|
867
|
-
return;
|
|
703
|
+
return cmdAgent(token, chatId, args.slice(1), chatState, setServerForChat, setActiveSessionForChat);
|
|
868
704
|
}
|
|
869
705
|
|
|
870
706
|
try {
|
|
@@ -1638,7 +1474,7 @@ async function cmdServe(token, chatId, args, chatState, setServerForChat, setAct
|
|
|
1638
1474
|
} else {
|
|
1639
1475
|
await sendLongMessage(token, chatState,
|
|
1640
1476
|
`⚠️ Server already exists for \`${relPath}\`.\n` +
|
|
1641
|
-
'Use `/
|
|
1477
|
+
'Use `/server <name>` to connect to it.');
|
|
1642
1478
|
}
|
|
1643
1479
|
return;
|
|
1644
1480
|
}
|
|
@@ -1836,6 +1672,210 @@ async function cmdModel(token, chatId, args, chatState, setServerForChat, setAct
|
|
|
1836
1672
|
}
|
|
1837
1673
|
}
|
|
1838
1674
|
|
|
1675
|
+
async function cmdServer(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
1676
|
+
if (args.length === 0) {
|
|
1677
|
+
await sendLongMessage(token, chatState,
|
|
1678
|
+
'Usage: `/server <name>`\n' +
|
|
1679
|
+
'To force-switch (take over a server from another chat), use `/server --force <name>`.\n' +
|
|
1680
|
+
'`<name>` can be a full directory path or a basename.\n' +
|
|
1681
|
+
'Use `/servers` to list available servers.');
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
const forceIdx = args.indexOf('--force');
|
|
1685
|
+
const force = forceIdx !== -1;
|
|
1686
|
+
const nameArgs = args.filter((a, i) => a !== '--force');
|
|
1687
|
+
if (nameArgs.length === 0) {
|
|
1688
|
+
await sendLongMessage(token, chatState,
|
|
1689
|
+
'Usage: `/server <name>`\n' +
|
|
1690
|
+
'To force-switch, use `/server --force <name>`.');
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
const name = nameArgs.join(' ');
|
|
1694
|
+
const servers = listServers();
|
|
1695
|
+
if (servers.length === 0) {
|
|
1696
|
+
await sendLongMessage(token, chatState, 'No servers running.');
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
let matches = servers.filter(s => s.dir.toLowerCase() === name.toLowerCase());
|
|
1700
|
+
if (matches.length === 0) {
|
|
1701
|
+
matches = servers.filter(s => path.basename(s.dir).toLowerCase() === name.toLowerCase());
|
|
1702
|
+
}
|
|
1703
|
+
if (matches.length === 0) {
|
|
1704
|
+
const lowerName = name.toLowerCase();
|
|
1705
|
+
matches = servers.filter(s => s.dir.toLowerCase().includes(lowerName) || path.basename(s.dir).toLowerCase().includes(lowerName));
|
|
1706
|
+
}
|
|
1707
|
+
if (matches.length === 0) {
|
|
1708
|
+
await sendLongMessage(token, chatState, `No server found matching "${name}".`);
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
if (matches.length > 1) {
|
|
1712
|
+
let exact = servers.filter(s => s.dir === name);
|
|
1713
|
+
if (exact.length === 0) exact = servers.filter(s => path.basename(s.dir) === name);
|
|
1714
|
+
if (exact.length === 1) {
|
|
1715
|
+
matches = exact;
|
|
1716
|
+
} else {
|
|
1717
|
+
const list = matches.map(s => ` ${s.url} → ${s.dir}`).join('\n');
|
|
1718
|
+
await sendLongMessage(token, chatState,
|
|
1719
|
+
`Multiple matches for "${name}":\n${list}\n\nPlease be more specific.`);
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
const target = matches[0];
|
|
1724
|
+
const existing = serverOwners[target.url];
|
|
1725
|
+
if (existing && existing.chatId !== chatState.chatId && !force) {
|
|
1726
|
+
await sendLongMessage(token, chatState,
|
|
1727
|
+
`⚠️ Server \`${target.url}\` is already connected to another chat.\n\n` +
|
|
1728
|
+
`Use \`/server --force ${name}\` to take over (the other chat will be disconnected).`);
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
if (force && existing && existing.chatId !== chatState.chatId) {
|
|
1732
|
+
const oldState = getChatState(existing.chatId, existing.threadId);
|
|
1733
|
+
oldState.serverBase = null;
|
|
1734
|
+
sendLongMessage(token, oldState,
|
|
1735
|
+
`⚠️ Server \`${target.url}\` has been taken over by another chat.\n\n` +
|
|
1736
|
+
`Use \`/servers\` to see available servers and reconnect.`).catch(() => {});
|
|
1737
|
+
}
|
|
1738
|
+
const pending = await setServerForChat(chatState, target.url);
|
|
1739
|
+
activateServer(target.dir, target.index, target.url);
|
|
1740
|
+
let msg = `Switched to server: ${target.url} → ${target.dir}`;
|
|
1741
|
+
if (pending && pending.trim()) {
|
|
1742
|
+
await sendLongMessage(token, chatState, msg);
|
|
1743
|
+
await sendLongMessage(token, chatState, pending.trim());
|
|
1744
|
+
} else {
|
|
1745
|
+
await sendLongMessage(token, chatState, msg);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
async function cmdSession(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
1750
|
+
const server = chatState.serverBase;
|
|
1751
|
+
if (args.length === 0) {
|
|
1752
|
+
await sendLongMessage(token, chatState,
|
|
1753
|
+
'`/session <name-or-number>` ─ switch to a session\n' +
|
|
1754
|
+
'`/session new [name]` ─ create a new session\n' +
|
|
1755
|
+
'`/session rename <name>` ─ rename the active session\n' +
|
|
1756
|
+
'Use `/sessions` to list available sessions.');
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
const sub = args[0].toLowerCase();
|
|
1760
|
+
if (sub === 'new') {
|
|
1761
|
+
const title = args.slice(1).join(' ') || undefined;
|
|
1762
|
+
try {
|
|
1763
|
+
const session = await createSession(server, title);
|
|
1764
|
+
const name = session.title || session.slug || session.id;
|
|
1765
|
+
setActiveSessionForChat(chatState, session.id, name);
|
|
1766
|
+
selectSession(server, session.id);
|
|
1767
|
+
await sendLongMessage(token, chatState,
|
|
1768
|
+
`Created session: \`${name}\`\nIt is now the active session.`);
|
|
1769
|
+
} catch (err) {
|
|
1770
|
+
await sendLongMessage(token, chatState, `Error creating session: ${err.message}`);
|
|
1771
|
+
}
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
if (sub === 'rename') {
|
|
1775
|
+
if (args.length < 2) {
|
|
1776
|
+
await sendLongMessage(token, chatState, 'Usage: `/session rename <name>`');
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
const st = getState(server);
|
|
1780
|
+
if (!st.activeSessionId) {
|
|
1781
|
+
await sendLongMessage(token, chatState, 'No active session. Use `/session <name>` to select one first.');
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
const newTitle = args.slice(1).join(' ');
|
|
1785
|
+
try {
|
|
1786
|
+
const session = await updateSession(server, st.activeSessionId, newTitle);
|
|
1787
|
+
const name = session.title || session.slug || session.id;
|
|
1788
|
+
await sendLongMessage(token, chatState, `Renamed session to: \`${name}\``);
|
|
1789
|
+
} catch (err) {
|
|
1790
|
+
await sendLongMessage(token, chatState, `Error renaming session: ${err.message}`);
|
|
1791
|
+
}
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
// Switch to session by name or number
|
|
1795
|
+
const query = args.join(' ');
|
|
1796
|
+
try {
|
|
1797
|
+
const projectDir = serverDir(server);
|
|
1798
|
+
const sessions = await listSessions(server, projectDir);
|
|
1799
|
+
const activeId = getState(server).activeSessionId;
|
|
1800
|
+
const filtered = sessions.filter(s =>
|
|
1801
|
+
(s.agent || s.directory || s.id === activeId) && (!projectDir || s.directory === projectDir));
|
|
1802
|
+
const sorted = sortedSessions(filtered, 50);
|
|
1803
|
+
const num = parseInt(query, 10);
|
|
1804
|
+
if (/^\d+$/.test(query) && num >= 1 && num <= sorted.length) {
|
|
1805
|
+
const target = sorted[num - 1];
|
|
1806
|
+
setActiveSessionForChat(chatState, target.id, sessionName(target));
|
|
1807
|
+
selectSession(server, target.id);
|
|
1808
|
+
await sendLongMessage(token, chatState,
|
|
1809
|
+
`Switched to session #${num}: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
const q = query.toLowerCase();
|
|
1813
|
+
const matches = sorted.filter(s => sessionName(s).toLowerCase().includes(q));
|
|
1814
|
+
if (matches.length === 0) {
|
|
1815
|
+
await sendLongMessage(token, chatState,
|
|
1816
|
+
`No session matching "${query}". Use /sessions to list active sessions.`);
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
if (matches.length > 1) {
|
|
1820
|
+
const list = matches.map(s => ` \`${sessionName(s)}\``).join('\n');
|
|
1821
|
+
await sendLongMessage(token, chatState,
|
|
1822
|
+
`Multiple matches for "${query}":\n${list}\n\nPlease be more specific or use a number.`);
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
const target = matches[0];
|
|
1826
|
+
setActiveSessionForChat(chatState, target.id);
|
|
1827
|
+
selectSession(server, target.id);
|
|
1828
|
+
await sendLongMessage(token, chatState,
|
|
1829
|
+
`Switched to session: \`${sessionName(target)}\`\nSubsequent messages will be sent to this session.`);
|
|
1830
|
+
} catch (err) {
|
|
1831
|
+
await sendLongMessage(token, chatState, `Error switching session: ${err.message}`);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
async function cmdAgent(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
1836
|
+
const server = chatState.serverBase;
|
|
1837
|
+
if (args.length === 0) {
|
|
1838
|
+
await sendLongMessage(token, chatState,
|
|
1839
|
+
'Usage: `/agent <name>`\n' +
|
|
1840
|
+
'Use `/agents` to list available agents.');
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
const name = args.join(' ');
|
|
1844
|
+
try {
|
|
1845
|
+
const agents = await listAgents(server);
|
|
1846
|
+
if (!agents || agents.length === 0) {
|
|
1847
|
+
await sendLongMessage(token, chatState, 'No agents available.');
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
let matches = agents.filter(a => a.name === name);
|
|
1851
|
+
if (matches.length === 0) {
|
|
1852
|
+
matches = agents.filter(a => a.name.toLowerCase().includes(name.toLowerCase()));
|
|
1853
|
+
}
|
|
1854
|
+
if (matches.length === 0) {
|
|
1855
|
+
await sendLongMessage(token, chatState, `No agent matching "${name}".`);
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
if (matches.length > 1) {
|
|
1859
|
+
const list = matches.map(a => ` \`${a.name}\``).join('\n');
|
|
1860
|
+
await sendLongMessage(token, chatState,
|
|
1861
|
+
`Multiple matches for "${name}":\n${list}\n\nPlease be more specific.`);
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
const target = matches[0];
|
|
1865
|
+
getState(server).activeAgentName = target.name;
|
|
1866
|
+
const st = getState(server);
|
|
1867
|
+
if (st.activeSessionId) {
|
|
1868
|
+
updateSession(server, st.activeSessionId, undefined, target.name);
|
|
1869
|
+
selectSession(server, st.activeSessionId);
|
|
1870
|
+
}
|
|
1871
|
+
const model = target.model ? ` ─ \`${target.model.providerID}/${target.model.modelID}\`` : '';
|
|
1872
|
+
await sendLongMessage(token, chatState,
|
|
1873
|
+
`Switched to agent: **${target.name}**${model}\nSubsequent messages will use this agent.`);
|
|
1874
|
+
} catch (err) {
|
|
1875
|
+
await sendLongMessage(token, chatState, `Error switching agent: ${err.message}`);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1839
1879
|
async function cmdMarkdown(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat, replyTo, replyToMsgId) {
|
|
1840
1880
|
let entry = null;
|
|
1841
1881
|
if (replyToMsgId) {
|
|
@@ -1859,10 +1899,11 @@ async function cmdMarkdown(token, chatId, args, chatState, setServerForChat, set
|
|
|
1859
1899
|
const COMMANDS = {
|
|
1860
1900
|
'help': cmdHelp,
|
|
1861
1901
|
'servers': cmdServers,
|
|
1862
|
-
'
|
|
1902
|
+
'server': cmdServer,
|
|
1863
1903
|
'sessions': cmdSessions,
|
|
1864
|
-
'session':
|
|
1904
|
+
'session': cmdSession,
|
|
1865
1905
|
'agents': cmdAgents,
|
|
1906
|
+
'agent': cmdAgent,
|
|
1866
1907
|
'models': cmdModels,
|
|
1867
1908
|
'model': cmdModel,
|
|
1868
1909
|
'think': cmdThink,
|
|
@@ -2338,7 +2379,7 @@ async function main() {
|
|
|
2338
2379
|
? `⚠️ Server \`${url}\` is unreachable. The message has been requeued and will be sent when the server is available again.\n\n` +
|
|
2339
2380
|
`**Options:**\n` +
|
|
2340
2381
|
`• Restart it: \`/resurrect\`\n` +
|
|
2341
|
-
`• Switch to another server: \`/
|
|
2382
|
+
`• Switch to another server: \`/server <name>\`\n` +
|
|
2342
2383
|
`• Flush the queue: \`/flush\``
|
|
2343
2384
|
: `Error: ${err.message}`;
|
|
2344
2385
|
try { await sendLongMessage(token, chatState, msg, replyTo, threadId); } catch (_) {}
|
|
@@ -2576,7 +2617,7 @@ async function main() {
|
|
|
2576
2617
|
await sendLongMessage(token, cs,
|
|
2577
2618
|
'🔄 Bot started. Previously connected to `' + dir + '`.\n' +
|
|
2578
2619
|
'⚠️ The server at `' + state.url + '` is not reachable.\n' +
|
|
2579
|
-
'Use `/resurrect` to restart it, or `/
|
|
2620
|
+
'Use `/resurrect` to restart it, or `/server <name>` to connect elsewhere.');
|
|
2580
2621
|
} catch (_) {}
|
|
2581
2622
|
}
|
|
2582
2623
|
}
|
|
@@ -2725,7 +2766,7 @@ async function main() {
|
|
|
2725
2766
|
`Your file upload has been queued (#${qlen}). It will be sent when the server becomes available.\n\n` +
|
|
2726
2767
|
`**Options:**\n` +
|
|
2727
2768
|
`• Restart it: \`/resurrect\`\n` +
|
|
2728
|
-
`• Switch to another server: \`/
|
|
2769
|
+
`• Switch to another server: \`/server <name>\`\n` +
|
|
2729
2770
|
`• Flush the queue: \`/flush\``);
|
|
2730
2771
|
continue;
|
|
2731
2772
|
}
|
|
@@ -2875,11 +2916,11 @@ async function main() {
|
|
|
2875
2916
|
const cmdArgs = parts.slice(1);
|
|
2876
2917
|
const handler = cmds[cmd];
|
|
2877
2918
|
if (handler) {
|
|
2878
|
-
// Disconnected guard:
|
|
2879
|
-
if (!cs.serverBase && cmd !== 'help' && cmd !== 'servers' && cmd !== '
|
|
2919
|
+
// Disconnected guard: /help, /servers, /server, /start, /comment, /shutdown, /serve, /new, and /resurrect work without a server
|
|
2920
|
+
if (!cs.serverBase && cmd !== 'help' && cmd !== 'servers' && cmd !== 'server' && cmd !== 'start' && cmd !== 'comment' && cmd !== 'shutdown' && cmd !== 'serve' && cmd !== 'new' && cmd !== 'resurrect') {
|
|
2880
2921
|
await sendLongMessage(token, cs,
|
|
2881
2922
|
'🔌 Not connected to any server.\n\n' +
|
|
2882
|
-
'Use `/servers` to see available servers and `/
|
|
2923
|
+
'Use `/servers` to see available servers and `/server <name>` to connect.');
|
|
2883
2924
|
continue;
|
|
2884
2925
|
}
|
|
2885
2926
|
try {
|
|
@@ -2891,7 +2932,7 @@ async function main() {
|
|
|
2891
2932
|
}
|
|
2892
2933
|
} else {
|
|
2893
2934
|
await sendLongMessage(token, cs,
|
|
2894
|
-
`Unknown command: /${cmd}\n\nAvailable commands: /help, /status, /servers, /sessions, /agents, /models, /model, /think, /record, /queue, /flush, /note, /comment, /disconnect, /serve, /new, /markdown`);
|
|
2935
|
+
`Unknown command: /${cmd}\n\nAvailable commands: /help, /status, /servers, /server, /sessions, /session, /agents, /agent, /models, /model, /think, /record, /queue, /flush, /note, /comment, /disconnect, /serve, /new, /markdown`);
|
|
2895
2936
|
}
|
|
2896
2937
|
continue;
|
|
2897
2938
|
}
|
|
@@ -2900,7 +2941,7 @@ async function main() {
|
|
|
2900
2941
|
if (!cs.serverBase) {
|
|
2901
2942
|
await sendLongMessage(token, cs,
|
|
2902
2943
|
'🔌 Not connected to any server.\n\n' +
|
|
2903
|
-
'Use `/servers` to see available servers and `/
|
|
2944
|
+
'Use `/servers` to see available servers and `/server <name>` to connect.');
|
|
2904
2945
|
continue;
|
|
2905
2946
|
}
|
|
2906
2947
|
|
|
@@ -2927,7 +2968,7 @@ async function main() {
|
|
|
2927
2968
|
const suggestions = [];
|
|
2928
2969
|
suggestions.push(`• Restart it: \`/resurrect\``);
|
|
2929
2970
|
if (servers.length > 0) {
|
|
2930
|
-
suggestions.push(`• Switch to another server: \`/
|
|
2971
|
+
suggestions.push(`• Switch to another server: \`/server <name>\``);
|
|
2931
2972
|
}
|
|
2932
2973
|
suggestions.push(`• Flush the queue: \`/flush\``);
|
|
2933
2974
|
await sendLongMessage(token, cs,
|
package/lib/ocmux.js
CHANGED
|
@@ -233,13 +233,29 @@ function resurrectServer(dir, printLogs) {
|
|
|
233
233
|
const actualName = windowNameByIndex(winIdx);
|
|
234
234
|
if (actualName !== dir) {
|
|
235
235
|
const found = windowByDir(dir);
|
|
236
|
-
if (found
|
|
236
|
+
if (found == null) {
|
|
237
|
+
// No window exists for this directory — old server is already dead, skip kill
|
|
238
|
+
winIdx = null;
|
|
239
|
+
} else {
|
|
240
|
+
winIdx = found;
|
|
241
|
+
}
|
|
237
242
|
}
|
|
238
243
|
if (winIdx != null) {
|
|
239
244
|
_tmux(['select-window', '-t', `${SESSION}:${winIdx}`]);
|
|
240
245
|
_tmux(['send-keys', '-t', `${SESSION}:${winIdx}.0`, 'C-c']);
|
|
241
246
|
sleep(0.3);
|
|
242
|
-
_tmux(['kill-window', '-t', `${SESSION}:${winIdx}`]);
|
|
247
|
+
const killR = _tmux(['kill-window', '-t', `${SESSION}:${winIdx}`]);
|
|
248
|
+
if (killR.status !== 0) {
|
|
249
|
+
throw new Error(`Failed to kill old tmux window ${winIdx}: ${killR.stderr || 'unknown error'}`);
|
|
250
|
+
}
|
|
251
|
+
// Wait for window to actually disappear (up to 2s)
|
|
252
|
+
for (let i = 0; i < 10; i++) {
|
|
253
|
+
if (windowByDir(dir) == null) break;
|
|
254
|
+
sleep(0.2);
|
|
255
|
+
}
|
|
256
|
+
if (windowByDir(dir) != null) {
|
|
257
|
+
throw new Error(`Old tmux window for ${dir} still exists after kill`);
|
|
258
|
+
}
|
|
243
259
|
}
|
|
244
260
|
}
|
|
245
261
|
}
|
|
@@ -260,11 +276,14 @@ function startServer(dir, printLogs) {
|
|
|
260
276
|
ensureSession();
|
|
261
277
|
const lf = logfileFor(dir);
|
|
262
278
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (winIdx
|
|
279
|
+
// Create a new tmux window and capture its index directly from the -P output
|
|
280
|
+
const newWinR = _tmux(['new-window', '-d', '-P', '-F', '#{window_index}', '-t', SESSION, '-n', dir, '-c', dir]);
|
|
281
|
+
if (newWinR.status !== 0) throw new Error(`failed to create tmux window for ${dir}`);
|
|
282
|
+
let winIdx = parseInt(newWinR.stdout.trim(), 10);
|
|
283
|
+
if (isNaN(winIdx)) {
|
|
284
|
+
winIdx = windowByDir(dir);
|
|
285
|
+
if (winIdx == null) throw new Error('could not determine new window index');
|
|
286
|
+
}
|
|
268
287
|
|
|
269
288
|
pinWindowName(winIdx);
|
|
270
289
|
|
|
@@ -283,8 +302,7 @@ function startServer(dir, printLogs) {
|
|
|
283
302
|
}
|
|
284
303
|
|
|
285
304
|
if (!url) {
|
|
286
|
-
|
|
287
|
-
if (wi != null) _tmux(['kill-window', '-t', `${SESSION}:${wi}`]);
|
|
305
|
+
_tmux(['kill-window', '-t', `${SESSION}:${winIdx}`]);
|
|
288
306
|
throw new Error('opencode server did not start within 10 seconds');
|
|
289
307
|
}
|
|
290
308
|
|