Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 83 additions & 1 deletion src/bridge/command-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export class CommandHandler {
'`/model claude`, `/model kimi`, or `/model codex` - Switch engine (resets session)',
'`/model <name>` - Set model for current engine',
'`/memory` - Memory document commands',
'`/sessions` - List all sessions in this chat',
'`/switch [N]` - Switch to session N (or show list to pick)',
'`/session <prefix>` - Switch by session ID prefix',
'`/help` - Show this help message',
'',
'**Agent Commands** (pass through to the agent — Claude only):',
Expand Down Expand Up @@ -95,7 +98,8 @@ export class CommandHandler {
} catch (err) {
this.logger.warn({ err, chatId }, 'Failed to release persistent executor on /reset');
}
await this.sender.sendTextNotice(chatId, '✅ Session Reset', 'Conversation cleared. Working directory preserved.', 'green');
await this.sender.sendTextNotice(chatId, '✅ Session Reset',
'New session created. Previous sessions preserved — use `/sessions` to switch back.', 'green');
return true;

case '/stop': {
Expand Down Expand Up @@ -162,6 +166,23 @@ export class CommandHandler {
return true;
}

case '/sessions': {
await this.handleSessionsCommand(chatId);
return true;
}

case '/switch': {
const args = text.slice('/switch'.length).trim();
await this.handleSwitchCommand(chatId, args);
return true;
}

case '/session': {
const args = text.slice('/session'.length).trim();
await this.handleSessionPrefixCommand(chatId, args);
return true;
}

default:
// Unrecognized /xxx commands — not handled here, pass through to Claude
return false;
Expand Down Expand Up @@ -409,6 +430,67 @@ export class CommandHandler {
);
}

private async handleSessionsCommand(chatId: string): Promise<void> {
const sessions = this.sessionManager.listSessions(chatId);
if (sessions.length === 0) {
await this.sender.sendTextNotice(chatId, '📋 Sessions', 'No sessions yet. Send a message to start one.');
return;
}
const lines = sessions.map(s => {
const marker = s.isActive ? ' ▶' : '';
const sid = s.sessionId ? ` \`${s.sessionId.slice(0, 8)}\`` : '';
const title = s.title ? ` — ${s.title}` : '';
const time = new Date(s.lastUsed).toLocaleString();
return `**${s.index + 1}.**${sid}${title} (${time})${marker}`;
});
lines.push('', 'Use `/switch N` to switch, or `/session <prefix>` to switch by session ID.');
await this.sender.sendTextNotice(chatId, '📋 Sessions', lines.join('\n'));
}

private async handleSwitchCommand(chatId: string, args: string): Promise<void> {
if (!args) {
await this.handleSessionsCommand(chatId);
return;
}
const num = parseInt(args, 10);
if (isNaN(num) || num < 1) {
await this.sender.sendTextNotice(chatId, '❌ Invalid', 'Usage: `/switch N` where N is the session number from `/sessions`.', 'red');
return;
}
const success = this.sessionManager.switchSession(chatId, num - 1);
if (success) {
// Release persistent executor so the next message picks up the
// switched session's sessionId instead of the old process's.
try { await this.releaseExecutor(chatId, 'session-switch'); } catch {}
const session = this.sessionManager.getSession(chatId);
const sid = session.sessionId ? `\`${session.sessionId.slice(0, 8)}...\`` : '_new_';
await this.sender.sendTextNotice(chatId, '✅ Switched', `Switched to session ${num} (${sid}).`, 'green');
} else {
const sessions = this.sessionManager.listSessions(chatId);
await this.sender.sendTextNotice(chatId, '❌ Invalid',
`Session ${num} does not exist. You have ${sessions.length} session(s). Use \`/sessions\` to list.`, 'red');
}
}

private async handleSessionPrefixCommand(chatId: string, prefix: string): Promise<void> {
if (!prefix || prefix.length < 8) {
await this.sender.sendTextNotice(chatId, '❌ Invalid',
'Usage: `/session <prefix>` — provide at least 8 characters of the session ID.', 'red');
return;
}
const idx = this.sessionManager.switchToSessionByPrefix(chatId, prefix);
if (idx >= 0) {
try { await this.releaseExecutor(chatId, 'session-switch'); } catch {}
const session = this.sessionManager.getSession(chatId);
const sid = session.sessionId ? `\`${session.sessionId.slice(0, 8)}...\`` : '_new_';
await this.sender.sendTextNotice(chatId, '✅ Switched',
`Switched to session ${idx + 1} (${sid}).`, 'green');
} else {
await this.sender.sendTextNotice(chatId, '❌ Not Found',
`No session found matching prefix \`${prefix}\`. Use \`/sessions\` to list available sessions.`, 'red');
}
}

private defaultModelForEngine(engine: EngineName): string | undefined {
switch (engine) {
case 'claude':
Expand Down
5 changes: 4 additions & 1 deletion src/bridge/message-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2325,10 +2325,13 @@ export class MessageBridge {
*/
/** Record session and messages in the cross-platform registry. */
private recordSession(chatId: string, prompt: string, responseText: string | undefined, claudeSessionId: string | undefined, costUsd: number | undefined, durationMs: number | undefined): void {
// Set session title from first user message
this.sessionManager.setTitle(chatId, prompt.slice(0, 50));

if (!this.sessionRegistry) return;
try {
this.sessionRegistry.createOrUpdate({
chatId,
chatId: this.sessionManager.getVirtualChatId(chatId),
botName: this.config.name,
claudeSessionId,
workingDirectory: this.sessionManager.getSession(chatId).workingDirectory,
Expand Down
Loading