feat: make /transfer bidirectional across telegram and tui
This commit is contained in:
@@ -486,7 +486,7 @@ pnpm tui:fs
|
|||||||
| `/verbose` | Toggle verbose output mode |
|
| `/verbose` | Toggle verbose output mode |
|
||||||
| `/pair` | Generate/list/revoke DM pairing codes |
|
| `/pair` | Generate/list/revoke DM pairing codes |
|
||||||
| `/fullscreen` | Switch to fullscreen mode |
|
| `/fullscreen` | Switch to fullscreen mode |
|
||||||
| `/transfer <dest>` | Transfer session to another frontend |
|
| `/transfer <dest>` | Transfer session to another frontend (`telegram` or `tui`) |
|
||||||
| `/quit` | Exit |
|
| `/quit` | Exit |
|
||||||
|
|
||||||
#### Runtime Model Switching
|
#### Runtime Model Switching
|
||||||
@@ -1476,7 +1476,7 @@ Exit code is `1` if any check fails, `0` otherwise. Checks that depend on a vali
|
|||||||
- Sessions persist in `~/.local/share/flynn/sessions.db`
|
- Sessions persist in `~/.local/share/flynn/sessions.db`
|
||||||
- Session ID format: `{frontend}:{userId}` (e.g., `telegram:123456789`)
|
- Session ID format: `{frontend}:{userId}` (e.g., `telegram:123456789`)
|
||||||
- History survives restarts
|
- History survives restarts
|
||||||
- Transfer sessions between frontends with `/transfer`
|
- Transfer sessions between frontends with `/transfer <telegram|tui>`
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
+24
-2
@@ -5107,10 +5107,32 @@
|
|||||||
"docs/plans/state.json"
|
"docs/plans/state.json"
|
||||||
],
|
],
|
||||||
"test_status": "pnpm test:run src/backends/native/agent.test.ts passing"
|
"test_status": "pnpm test:run src/backends/native/agent.test.ts passing"
|
||||||
|
},
|
||||||
|
"bidirectional-frontend-transfer-command": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-18",
|
||||||
|
"updated": "2026-02-18",
|
||||||
|
"summary": "Promoted `/transfer` to a first-class command-registry command and implemented bidirectional `telegram <-> tui` session transfer semantics across channel and TUI paths. Added Telegram `/transfer` command ingestion (including `/transfer@bot ...` parsing), transfer fast-path handling in daemon routing, improved TUI transfer usage/no-op messaging, and expanded regression coverage.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/commands/types.ts",
|
||||||
|
"src/commands/index.ts",
|
||||||
|
"src/commands/builtin/index.ts",
|
||||||
|
"src/commands/builtin/index.test.ts",
|
||||||
|
"src/daemon/routing.ts",
|
||||||
|
"src/daemon/routing.test.ts",
|
||||||
|
"src/channels/telegram/adapter.ts",
|
||||||
|
"src/channels/telegram/adapter.test.ts",
|
||||||
|
"src/frontends/tui/commands.ts",
|
||||||
|
"src/frontends/tui/commands.test.ts",
|
||||||
|
"src/cli/tui.ts",
|
||||||
|
"README.md",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm test:run src/commands/builtin/index.test.ts src/daemon/routing.test.ts src/channels/telegram/adapter.test.ts src/frontends/tui/commands.test.ts passing + pnpm typecheck passing"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"overall_progress": {
|
"overall_progress": {
|
||||||
"total_test_count": 1895,
|
"total_test_count": 1900,
|
||||||
"all_tests_passing": true,
|
"all_tests_passing": true,
|
||||||
"p0_completion": "3/3 (100%)",
|
"p0_completion": "3/3 (100%)",
|
||||||
"p1_completion": "4/4 (100%)",
|
"p1_completion": "4/4 (100%)",
|
||||||
@@ -5130,7 +5152,7 @@
|
|||||||
"gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram",
|
"gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram",
|
||||||
"native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback",
|
"native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback",
|
||||||
"remaining_phases_completion": "Phase 1: 3/3 (100%) — context levels, command registry, memory structure. Phase 2: 3/3 (100%) — component registry, confidence routing, history index. Phase 3: 2/2 (100%) — adaptive memory/compaction, truthfulness/autonomy hardening",
|
"remaining_phases_completion": "Phase 1: 3/3 (100%) — context levels, command registry, memory structure. Phase 2: 3/3 (100%) — component registry, confidence routing, history index. Phase 3: 2/2 (100%) — adaptive memory/compaction, truthfulness/autonomy hardening",
|
||||||
"next_up": "Monitor production feedback for the expanded sessions operator surface and prioritize next post-parity slice from reliability and capability roadmap"
|
"next_up": "Monitor production feedback for bidirectional session-transfer command behavior across Telegram/TUI and prioritize the next post-parity reliability/capability slice"
|
||||||
},
|
},
|
||||||
"soul_md_and_cron_create": {
|
"soul_md_and_cron_create": {
|
||||||
"date": "2026-02-11",
|
"date": "2026-02-11",
|
||||||
|
|||||||
@@ -78,13 +78,14 @@ describe('TelegramAdapter', () => {
|
|||||||
|
|
||||||
// .use() for auth middleware
|
// .use() for auth middleware
|
||||||
expect(mockUse).toHaveBeenCalledTimes(1);
|
expect(mockUse).toHaveBeenCalledTimes(1);
|
||||||
// .command() for /start, /reset, /model, /local, /cloud
|
// .command() for /start, /reset, /model, /local, /cloud, /transfer
|
||||||
expect(mockCommand).toHaveBeenCalledTimes(5);
|
expect(mockCommand).toHaveBeenCalledTimes(6);
|
||||||
expect(mockCommand.mock.calls[0][0]).toBe('start');
|
expect(mockCommand.mock.calls[0][0]).toBe('start');
|
||||||
expect(mockCommand.mock.calls[1][0]).toBe('reset');
|
expect(mockCommand.mock.calls[1][0]).toBe('reset');
|
||||||
expect(mockCommand.mock.calls[2][0]).toBe('model');
|
expect(mockCommand.mock.calls[2][0]).toBe('model');
|
||||||
expect(mockCommand.mock.calls[3][0]).toBe('local');
|
expect(mockCommand.mock.calls[3][0]).toBe('local');
|
||||||
expect(mockCommand.mock.calls[4][0]).toBe('cloud');
|
expect(mockCommand.mock.calls[4][0]).toBe('cloud');
|
||||||
|
expect(mockCommand.mock.calls[5][0]).toBe('transfer');
|
||||||
// .on('message:text', ...) for text handler
|
// .on('message:text', ...) for text handler
|
||||||
expect(mockOn).toHaveBeenCalledWith('message:text', expect.any(Function));
|
expect(mockOn).toHaveBeenCalledWith('message:text', expect.any(Function));
|
||||||
// .start() to begin long polling
|
// .start() to begin long polling
|
||||||
@@ -269,6 +270,34 @@ describe('TelegramAdapter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('/transfer command strips @bot suffix in groups', async () => {
|
||||||
|
const handler = vi.fn();
|
||||||
|
adapter.onMessage(handler);
|
||||||
|
|
||||||
|
await adapter.connect();
|
||||||
|
|
||||||
|
const transferCall = mockCommand.mock.calls.find((call) => call[0] === 'transfer');
|
||||||
|
expect(transferCall).toBeDefined();
|
||||||
|
const transferHandler = getCommandHandler('transfer');
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
message: { message_id: 124, text: '/transfer@flynn_bot tui' },
|
||||||
|
chat: { id: 100 },
|
||||||
|
from: { first_name: 'Will' },
|
||||||
|
};
|
||||||
|
|
||||||
|
await transferHandler(ctx);
|
||||||
|
|
||||||
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
|
const msg: InboundMessage = handler.mock.calls[0][0];
|
||||||
|
expect(msg.text).toBe('/transfer tui');
|
||||||
|
expect(msg.metadata).toEqual({
|
||||||
|
isCommand: true,
|
||||||
|
command: 'transfer',
|
||||||
|
commandArgs: 'tui',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ── Auth middleware ───────────────────────────────────────────
|
// ── Auth middleware ───────────────────────────────────────────
|
||||||
|
|
||||||
it('auth middleware blocks unauthorized chat IDs', async () => {
|
it('auth middleware blocks unauthorized chat IDs', async () => {
|
||||||
|
|||||||
@@ -210,6 +210,28 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.bot.command('transfer', async (ctx) => {
|
||||||
|
if (!this.messageHandler) {return;}
|
||||||
|
|
||||||
|
// Telegram can deliver group commands in the form: /transfer@bot_username ...
|
||||||
|
// Strip optional @mention for consistent parsing across contexts.
|
||||||
|
const args = ctx.message?.text?.replace(/^\/transfer(?:@\S+)?\s*/i, '').trim() ?? '';
|
||||||
|
|
||||||
|
this.messageHandler({
|
||||||
|
id: String(ctx.message?.message_id ?? Date.now()),
|
||||||
|
channel: 'telegram',
|
||||||
|
senderId: String(ctx.chat.id),
|
||||||
|
senderName: ctx.from?.first_name,
|
||||||
|
text: `/transfer ${args}`.trim(),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
metadata: {
|
||||||
|
isCommand: true,
|
||||||
|
command: 'transfer',
|
||||||
|
commandArgs: args || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ── Text message handler ──
|
// ── Text message handler ──
|
||||||
|
|
||||||
this.bot.on('message:text', async (ctx) => {
|
this.bot.on('message:text', async (ctx) => {
|
||||||
|
|||||||
+17
-7
@@ -238,15 +238,25 @@ export function registerTuiCommand(program: Command): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const transferSessionToTarget = (target: string): string => {
|
const transferSessionToTarget = (target: string): string => {
|
||||||
if (target !== 'telegram') {
|
const normalizedTarget = target.trim().toLowerCase();
|
||||||
return `Unknown transfer target: ${target}`;
|
if (!normalizedTarget) {
|
||||||
|
return 'Usage: /transfer <tui|telegram>';
|
||||||
}
|
}
|
||||||
if (!config.telegram || config.telegram.allowed_chat_ids.length === 0) {
|
|
||||||
return 'Telegram not configured';
|
if (normalizedTarget === 'tui') {
|
||||||
|
return 'Session is already active on TUI (local)';
|
||||||
}
|
}
|
||||||
const telegramUserId = String(config.telegram.allowed_chat_ids[0]);
|
|
||||||
sessionManager.transferSession('tui', 'local', 'telegram', telegramUserId);
|
if (normalizedTarget === 'telegram') {
|
||||||
return `Session transferred to Telegram (${telegramUserId})`;
|
if (!config.telegram || config.telegram.allowed_chat_ids.length === 0) {
|
||||||
|
return 'Telegram not configured';
|
||||||
|
}
|
||||||
|
const telegramUserId = String(config.telegram.allowed_chat_ids[0]);
|
||||||
|
sessionManager.transferSession('tui', 'local', 'telegram', telegramUserId);
|
||||||
|
return `Session transferred to Telegram (${telegramUserId})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Unknown transfer target: ${target}. Supported targets: tui, telegram`;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (opts.fullscreen) {
|
if (opts.fullscreen) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
|
||||||
import { createContextCommand, createElevateCommand, createModelCommand, createQueueCommand, createResearchCommand } from './index.js';
|
import { createContextCommand, createElevateCommand, createModelCommand, createQueueCommand, createResearchCommand, createTransferCommand } from './index.js';
|
||||||
|
|
||||||
describe('builtin /model command', () => {
|
describe('builtin /model command', () => {
|
||||||
it('passes through the full argument string', async () => {
|
it('passes through the full argument string', async () => {
|
||||||
@@ -169,3 +169,31 @@ describe('builtin /context command', () => {
|
|||||||
expect(result).toEqual({ handled: true, text: 'Context command is not available in this session.' });
|
expect(result).toEqual({ handled: true, text: 'Context command is not available in this session.' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('builtin /transfer command', () => {
|
||||||
|
it('passes through the full target argument string', async () => {
|
||||||
|
const cmd = createTransferCommand();
|
||||||
|
const transferSession = vi.fn(() => 'Session transferred');
|
||||||
|
const result = await cmd.execute(['telegram'], {
|
||||||
|
channel: 'test',
|
||||||
|
senderId: 'user',
|
||||||
|
sessionId: 's1',
|
||||||
|
rawInput: '/transfer telegram',
|
||||||
|
services: { transferSession },
|
||||||
|
});
|
||||||
|
expect(transferSession).toHaveBeenCalledWith('telegram');
|
||||||
|
expect(result).toEqual({ handled: true, text: 'Session transferred' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns not-available when service is missing', async () => {
|
||||||
|
const cmd = createTransferCommand();
|
||||||
|
const result = await cmd.execute(['tui'], {
|
||||||
|
channel: 'test',
|
||||||
|
senderId: 'user',
|
||||||
|
sessionId: 's1',
|
||||||
|
rawInput: '/transfer tui',
|
||||||
|
services: {},
|
||||||
|
});
|
||||||
|
expect(result).toEqual({ handled: true, text: 'Transfer command is not available in this session.' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -218,6 +218,24 @@ export function createResearchCommand(): CommandDefinition {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createTransferCommand(): CommandDefinition {
|
||||||
|
return {
|
||||||
|
name: 'transfer',
|
||||||
|
description: 'Transfer session to another frontend',
|
||||||
|
execute: async (args, ctx) => {
|
||||||
|
if (!ctx.services?.transferSession) {
|
||||||
|
return notAvailable('Transfer command');
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = args.join(' ').trim();
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
text: await ctx.services.transferSession(target),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function registerBuiltinCommands(registry: CommandRegistry): void {
|
export function registerBuiltinCommands(registry: CommandRegistry): void {
|
||||||
registry.register(createHelpCommand(registry));
|
registry.register(createHelpCommand(registry));
|
||||||
registry.register(createStatusCommand());
|
registry.register(createStatusCommand());
|
||||||
@@ -229,4 +247,5 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
|
|||||||
registry.register(createResetCommand());
|
registry.register(createResetCommand());
|
||||||
registry.register(createElevateCommand());
|
registry.register(createElevateCommand());
|
||||||
registry.register(createQueueCommand());
|
registry.register(createQueueCommand());
|
||||||
|
registry.register(createTransferCommand());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ export {
|
|||||||
createCompactCommand,
|
createCompactCommand,
|
||||||
createResetCommand,
|
createResetCommand,
|
||||||
createQueueCommand,
|
createQueueCommand,
|
||||||
|
createTransferCommand,
|
||||||
registerBuiltinCommands,
|
registerBuiltinCommands,
|
||||||
} from './builtin/index.js';
|
} from './builtin/index.js';
|
||||||
|
|||||||
@@ -34,4 +34,5 @@ export interface CommandServices {
|
|||||||
getQueue?: () => Promise<string> | string;
|
getQueue?: () => Promise<string> | string;
|
||||||
setQueue?: (input: string) => Promise<string> | string;
|
setQueue?: (input: string) => Promise<string> | string;
|
||||||
resetQueue?: () => Promise<string> | string;
|
resetQueue?: () => Promise<string> | string;
|
||||||
|
transferSession?: (target: string) => Promise<string> | string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,6 +213,76 @@ describe('daemon command fast-path integration', () => {
|
|||||||
expect(session.deleteConfig).toHaveBeenCalledWith('modelTier');
|
expect(session.deleteConfig).toHaveBeenCalledWith('modelTier');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles /transfer command via command fast-path and copies session to TUI', async () => {
|
||||||
|
const processSpy = vi.spyOn(AgentOrchestrator.prototype, 'process');
|
||||||
|
const transferSpy = vi.fn();
|
||||||
|
const session = {
|
||||||
|
id: 'telegram:user-1',
|
||||||
|
addMessage: vi.fn(),
|
||||||
|
getHistory: vi.fn(() => []),
|
||||||
|
clear: vi.fn(),
|
||||||
|
replaceHistory: vi.fn(),
|
||||||
|
getConfig: vi.fn(() => undefined),
|
||||||
|
setConfig: vi.fn(),
|
||||||
|
deleteConfig: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const commandRegistry = new CommandRegistry();
|
||||||
|
registerBuiltinCommands(commandRegistry);
|
||||||
|
|
||||||
|
const router = createMessageRouter({
|
||||||
|
sessionManager: {
|
||||||
|
getSession: vi.fn(() => session),
|
||||||
|
transferSession: transferSpy,
|
||||||
|
} as unknown as MessageRouterDeps['sessionManager'],
|
||||||
|
modelRouter: {
|
||||||
|
getAvailableTiers: () => ['fast', 'default', 'complex', 'local'],
|
||||||
|
getAllLabels: () => ({ fast: 'fast', default: 'default', complex: 'complex', local: 'local' }),
|
||||||
|
getLabel: (tier: string) => tier,
|
||||||
|
} as unknown as MessageRouterDeps['modelRouter'],
|
||||||
|
systemPrompt: 'test prompt',
|
||||||
|
toolRegistry: {
|
||||||
|
clone() { return this; },
|
||||||
|
register: vi.fn(),
|
||||||
|
} as unknown as MessageRouterDeps['toolRegistry'],
|
||||||
|
toolExecutor: {} as unknown as MessageRouterDeps['toolExecutor'],
|
||||||
|
config: {
|
||||||
|
agents: {
|
||||||
|
primary_tier: 'default',
|
||||||
|
delegation: {
|
||||||
|
compaction: 'fast',
|
||||||
|
memory_extraction: 'fast',
|
||||||
|
classification: 'fast',
|
||||||
|
tool_summarisation: 'fast',
|
||||||
|
complex_reasoning: 'complex',
|
||||||
|
},
|
||||||
|
max_delegation_depth: 3,
|
||||||
|
max_iterations: 10,
|
||||||
|
},
|
||||||
|
compaction: { enabled: false },
|
||||||
|
models: { default: { provider: 'anthropic', model: 'claude' } },
|
||||||
|
} as unknown as MessageRouterDeps['config'],
|
||||||
|
commandRegistry,
|
||||||
|
});
|
||||||
|
|
||||||
|
const reply = vi.fn(async (_message: OutboundMessage) => {});
|
||||||
|
await router.handler({
|
||||||
|
id: 'm-transfer',
|
||||||
|
channel: 'telegram',
|
||||||
|
senderId: 'user-1',
|
||||||
|
text: '/transfer tui',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
metadata: { isCommand: true, command: 'transfer', commandArgs: 'tui' },
|
||||||
|
} as MessageRouterInput, reply);
|
||||||
|
|
||||||
|
expect(processSpy).not.toHaveBeenCalled();
|
||||||
|
expect(transferSpy).toHaveBeenCalledWith('telegram', 'user-1', 'tui', 'local');
|
||||||
|
expect(reply).toHaveBeenCalledWith({
|
||||||
|
text: 'Session transferred to TUI (local)',
|
||||||
|
replyTo: 'm-transfer',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('emits user.action audit events for channel messages', async () => {
|
it('emits user.action audit events for channel messages', async () => {
|
||||||
const mockAuditLogger = {
|
const mockAuditLogger = {
|
||||||
userAction: vi.fn(),
|
userAction: vi.fn(),
|
||||||
|
|||||||
+40
-1
@@ -274,7 +274,7 @@ export function createMessageRouter(deps: {
|
|||||||
tier: effectiveTier,
|
tier: effectiveTier,
|
||||||
autonomyLevel: deps.config.agents.autonomy_level ?? 'standard',
|
autonomyLevel: deps.config.agents.autonomy_level ?? 'standard',
|
||||||
sensitiveMode: deps.config.agents.sensitive_mode,
|
sensitiveMode: deps.config.agents.sensitive_mode,
|
||||||
immutableDenylist: deps.config.agents.immutable_denylist.map((rule) => ({
|
immutableDenylist: (deps.config.agents.immutable_denylist ?? []).map((rule) => ({
|
||||||
tool: rule.tool,
|
tool: rule.tool,
|
||||||
argsPattern: rule.args_pattern,
|
argsPattern: rule.args_pattern,
|
||||||
reason: rule.reason,
|
reason: rule.reason,
|
||||||
@@ -838,6 +838,45 @@ export function createMessageRouter(deps: {
|
|||||||
session.deleteConfig('queue.summarize_overflow');
|
session.deleteConfig('queue.summarize_overflow');
|
||||||
return 'Reset session queue overrides.';
|
return 'Reset session queue overrides.';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
transferSession: (targetRaw: string) => {
|
||||||
|
const target = targetRaw.trim().toLowerCase();
|
||||||
|
if (!target) {
|
||||||
|
return 'Usage: /transfer <tui|telegram>';
|
||||||
|
}
|
||||||
|
|
||||||
|
let toFrontend: string;
|
||||||
|
let toUserId: string;
|
||||||
|
let destinationLabel: string;
|
||||||
|
|
||||||
|
if (target === 'tui') {
|
||||||
|
toFrontend = 'tui';
|
||||||
|
toUserId = 'local';
|
||||||
|
destinationLabel = 'TUI (local)';
|
||||||
|
} else if (target === 'telegram') {
|
||||||
|
if (msg.channel === 'telegram') {
|
||||||
|
toFrontend = 'telegram';
|
||||||
|
toUserId = msg.senderId;
|
||||||
|
} else {
|
||||||
|
const chatId = deps.config.telegram?.allowed_chat_ids?.[0];
|
||||||
|
if (chatId === undefined) {
|
||||||
|
return 'Telegram not configured';
|
||||||
|
}
|
||||||
|
toFrontend = 'telegram';
|
||||||
|
toUserId = String(chatId);
|
||||||
|
}
|
||||||
|
destinationLabel = `Telegram (${toUserId})`;
|
||||||
|
} else {
|
||||||
|
return `Unknown transfer target: ${target}. Supported targets: tui, telegram`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.channel === toFrontend && msg.senderId === toUserId) {
|
||||||
|
return `Session is already active on ${destinationLabel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.sessionManager.transferSession(msg.channel, msg.senderId, toFrontend, toUserId);
|
||||||
|
return `Session transferred to ${destinationLabel}`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ describe('parseCommand', () => {
|
|||||||
|
|
||||||
it('parses /transfer command', () => {
|
it('parses /transfer command', () => {
|
||||||
expect(parseCommand('/transfer telegram')).toEqual({ type: 'transfer', target: 'telegram' });
|
expect(parseCommand('/transfer telegram')).toEqual({ type: 'transfer', target: 'telegram' });
|
||||||
|
expect(parseCommand('/transfer')).toEqual({ type: 'transfer', target: '' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parses /queue commands', () => {
|
it('parses /queue commands', () => {
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ export function parseCommand(input: string): Command | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Transfer
|
// Transfer
|
||||||
|
if (trimmed === '/transfer') {
|
||||||
|
return { type: 'transfer', target: '' };
|
||||||
|
}
|
||||||
if (trimmed.startsWith('/transfer ')) {
|
if (trimmed.startsWith('/transfer ')) {
|
||||||
const target = trimmed.slice('/transfer '.length).trim();
|
const target = trimmed.slice('/transfer '.length).trim();
|
||||||
return { type: 'transfer', target };
|
return { type: 'transfer', target };
|
||||||
@@ -172,7 +175,7 @@ Commands:
|
|||||||
/verbose Toggle verbose mode (show raw streaming and tool output)
|
/verbose Toggle verbose mode (show raw streaming and tool output)
|
||||||
/status Show session info and token usage
|
/status Show session info and token usage
|
||||||
/fullscreen, /fs Switch to fullscreen mode
|
/fullscreen, /fs Switch to fullscreen mode
|
||||||
/transfer <dest> Transfer session to another frontend
|
/transfer <dest> Transfer session to another frontend (telegram|tui)
|
||||||
/quit, /exit Exit TUI
|
/quit, /exit Exit TUI
|
||||||
`.trim();
|
`.trim();
|
||||||
}
|
}
|
||||||
@@ -224,7 +227,7 @@ export const COMMAND_TOOLTIPS: Record<string, string> = {
|
|||||||
'/pair': 'Generate/list/revoke DM pairing codes',
|
'/pair': 'Generate/list/revoke DM pairing codes',
|
||||||
'/queue': 'Show or update per-session queue policy',
|
'/queue': 'Show or update per-session queue policy',
|
||||||
'/elevate': 'Show or manage elevated mode',
|
'/elevate': 'Show or manage elevated mode',
|
||||||
'/transfer': 'Transfer session to another frontend',
|
'/transfer': 'Transfer session to another frontend (telegram|tui)',
|
||||||
'/quit': 'Exit TUI',
|
'/quit': 'Exit TUI',
|
||||||
'/exit': 'Exit TUI',
|
'/exit': 'Exit TUI',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user