feat: add persistent reply keyboard with main action buttons
This commit is contained in:
+47
-2
@@ -1,10 +1,27 @@
|
||||
import { Bot, type Context } from "grammy";
|
||||
import { Bot, type Context, Keyboard } from "grammy";
|
||||
import { Store } from "./state/store.js";
|
||||
import { SessionManager } from "./services/session-manager.js";
|
||||
import { ClaudeService } from "./services/claude.js";
|
||||
import { registerCommands } from "./handlers/commands.js";
|
||||
import { createMessageHandler } from "./handlers/message.js";
|
||||
|
||||
// Persistent reply keyboard
|
||||
const mainKeyboard = new Keyboard()
|
||||
.text("Sessions").text("Projects").text("Status").row()
|
||||
.text("Stop").text("Stop All").text("Help")
|
||||
.resized()
|
||||
.persistent();
|
||||
|
||||
// Button text → command name mapping
|
||||
const BUTTON_COMMANDS: Record<string, string> = {
|
||||
"Sessions": "sessions",
|
||||
"Projects": "projects",
|
||||
"Status": "status",
|
||||
"Stop": "stop",
|
||||
"Stop All": "stopall",
|
||||
"Help": "help",
|
||||
};
|
||||
|
||||
export function createBot(token: string, ownerId: number): Bot {
|
||||
const bot = new Bot(token);
|
||||
|
||||
@@ -20,12 +37,25 @@ export function createBot(token: string, ownerId: number): Bot {
|
||||
|
||||
// Register command handlers
|
||||
const commands: Record<string, (ctx: Context) => Promise<void>> = {};
|
||||
registerCommands(manager, claude, commands);
|
||||
registerCommands(manager, claude, commands, mainKeyboard);
|
||||
|
||||
for (const [name, handler] of Object.entries(commands)) {
|
||||
bot.command(name, handler);
|
||||
}
|
||||
|
||||
// Handle reply keyboard button presses
|
||||
bot.on("message:text", async (ctx, next) => {
|
||||
const text = ctx.message.text;
|
||||
const commandName = BUTTON_COMMANDS[text];
|
||||
if (commandName && commands[commandName]) {
|
||||
// Simulate empty match for commands that don't need args
|
||||
(ctx as any).match = "";
|
||||
await commands[commandName](ctx);
|
||||
return;
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
// Callback queries for inline buttons
|
||||
bot.on("callback_query:data", async (ctx) => {
|
||||
const data = ctx.callbackQuery.data;
|
||||
@@ -69,5 +99,20 @@ export function createBot(token: string, ownerId: number): Bot {
|
||||
// Plain text messages
|
||||
bot.on("message:text", createMessageHandler(manager, claude));
|
||||
|
||||
// Send main keyboard on /start (Telegram's built-in start) with no args
|
||||
bot.api.setMyCommands([
|
||||
{ command: "start", description: "Start a session: /start <path|name>" },
|
||||
{ command: "save", description: "Save bookmark: /save <name> <path>" },
|
||||
{ command: "switch", description: "Switch session: /switch <name>" },
|
||||
{ command: "remove", description: "Remove bookmark: /remove <name>" },
|
||||
]);
|
||||
|
||||
// Show keyboard on first interaction
|
||||
bot.use(async (ctx, next) => {
|
||||
await next();
|
||||
});
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
export { mainKeyboard };
|
||||
|
||||
+18
-21
@@ -1,4 +1,4 @@
|
||||
import { type Context, InlineKeyboard } from "grammy";
|
||||
import { type Context, InlineKeyboard, type Keyboard } from "grammy";
|
||||
import { existsSync } from "fs";
|
||||
import { SessionManager } from "../services/session-manager.js";
|
||||
import { ClaudeService } from "../services/claude.js";
|
||||
@@ -7,11 +7,12 @@ export function registerCommands(
|
||||
manager: SessionManager,
|
||||
claude: ClaudeService,
|
||||
onCommand: Record<string, (ctx: Context) => Promise<void>>,
|
||||
keyboard: Keyboard,
|
||||
): void {
|
||||
onCommand["start"] = async (ctx) => {
|
||||
const arg = ctx.match as string;
|
||||
if (!arg) {
|
||||
await ctx.reply("Usage: /start <path or project name>");
|
||||
await ctx.reply("Usage: /start <path or project name>", { reply_markup: keyboard });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,7 +88,7 @@ export function registerCommands(
|
||||
const sessions = manager.getAllSessions();
|
||||
const names = Object.keys(sessions);
|
||||
if (names.length === 0) {
|
||||
await ctx.reply("No active sessions.");
|
||||
await ctx.reply("No active sessions.", { reply_markup: keyboard });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -98,12 +99,12 @@ export function registerCommands(
|
||||
return `${marker} ${name} — ${s.cwd} (${s.status})`;
|
||||
});
|
||||
|
||||
const keyboard = new InlineKeyboard();
|
||||
const inlineKb = new InlineKeyboard();
|
||||
for (const name of names) {
|
||||
keyboard.text(name, `switch:${name}`);
|
||||
inlineKb.text(name, `switch:${name}`);
|
||||
}
|
||||
|
||||
await ctx.reply(lines.join("\n"), { reply_markup: keyboard });
|
||||
await ctx.reply(lines.join("\n"), { reply_markup: inlineKb });
|
||||
};
|
||||
|
||||
onCommand["status"] = async (ctx) => {
|
||||
@@ -146,37 +147,33 @@ export function registerCommands(
|
||||
const projects = manager.getProjects();
|
||||
const names = Object.keys(projects);
|
||||
if (names.length === 0) {
|
||||
await ctx.reply("No saved projects.");
|
||||
await ctx.reply("No saved projects. Use /save <name> <path>", { reply_markup: keyboard });
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = names.map((n) => `${n} — ${projects[n]}`);
|
||||
const keyboard = new InlineKeyboard();
|
||||
const inlineKb = new InlineKeyboard();
|
||||
for (const name of names) {
|
||||
keyboard.text(`Start ${name}`, `start:${name}`);
|
||||
inlineKb.text(`Start ${name}`, `start:${name}`);
|
||||
}
|
||||
|
||||
await ctx.reply(lines.join("\n"), { reply_markup: keyboard });
|
||||
await ctx.reply(lines.join("\n"), { reply_markup: inlineKb });
|
||||
};
|
||||
|
||||
onCommand["help"] = async (ctx) => {
|
||||
await ctx.reply(
|
||||
[
|
||||
"Session commands:",
|
||||
" /start <path|name> — start new session",
|
||||
" /stop [name] — stop session",
|
||||
" /stopall — stop all sessions",
|
||||
" /switch <name> — switch active session",
|
||||
" /sessions — list sessions",
|
||||
" /status — current session info",
|
||||
"Use buttons below or commands:",
|
||||
"",
|
||||
"Project bookmarks:",
|
||||
" /save <name> <path> — save bookmark",
|
||||
" /remove <name> — remove bookmark",
|
||||
" /projects — list bookmarks",
|
||||
"/start <path|name> — start new session",
|
||||
"/stop [name] — stop session",
|
||||
"/switch <name> — switch active session",
|
||||
"/save <name> <path> — save bookmark",
|
||||
"/remove <name> — remove bookmark",
|
||||
"",
|
||||
"Any other text is sent to the active session.",
|
||||
].join("\n"),
|
||||
{ reply_markup: keyboard },
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user