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 { Store } from "./state/store.js";
|
||||||
import { SessionManager } from "./services/session-manager.js";
|
import { SessionManager } from "./services/session-manager.js";
|
||||||
import { ClaudeService } from "./services/claude.js";
|
import { ClaudeService } from "./services/claude.js";
|
||||||
import { registerCommands } from "./handlers/commands.js";
|
import { registerCommands } from "./handlers/commands.js";
|
||||||
import { createMessageHandler } from "./handlers/message.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 {
|
export function createBot(token: string, ownerId: number): Bot {
|
||||||
const bot = new Bot(token);
|
const bot = new Bot(token);
|
||||||
|
|
||||||
@@ -20,12 +37,25 @@ export function createBot(token: string, ownerId: number): Bot {
|
|||||||
|
|
||||||
// Register command handlers
|
// Register command handlers
|
||||||
const commands: Record<string, (ctx: Context) => Promise<void>> = {};
|
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)) {
|
for (const [name, handler] of Object.entries(commands)) {
|
||||||
bot.command(name, handler);
|
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
|
// Callback queries for inline buttons
|
||||||
bot.on("callback_query:data", async (ctx) => {
|
bot.on("callback_query:data", async (ctx) => {
|
||||||
const data = ctx.callbackQuery.data;
|
const data = ctx.callbackQuery.data;
|
||||||
@@ -69,5 +99,20 @@ export function createBot(token: string, ownerId: number): Bot {
|
|||||||
// Plain text messages
|
// Plain text messages
|
||||||
bot.on("message:text", createMessageHandler(manager, claude));
|
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;
|
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 { existsSync } from "fs";
|
||||||
import { SessionManager } from "../services/session-manager.js";
|
import { SessionManager } from "../services/session-manager.js";
|
||||||
import { ClaudeService } from "../services/claude.js";
|
import { ClaudeService } from "../services/claude.js";
|
||||||
@@ -7,11 +7,12 @@ export function registerCommands(
|
|||||||
manager: SessionManager,
|
manager: SessionManager,
|
||||||
claude: ClaudeService,
|
claude: ClaudeService,
|
||||||
onCommand: Record<string, (ctx: Context) => Promise<void>>,
|
onCommand: Record<string, (ctx: Context) => Promise<void>>,
|
||||||
|
keyboard: Keyboard,
|
||||||
): void {
|
): void {
|
||||||
onCommand["start"] = async (ctx) => {
|
onCommand["start"] = async (ctx) => {
|
||||||
const arg = ctx.match as string;
|
const arg = ctx.match as string;
|
||||||
if (!arg) {
|
if (!arg) {
|
||||||
await ctx.reply("Usage: /start <path or project name>");
|
await ctx.reply("Usage: /start <path or project name>", { reply_markup: keyboard });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ export function registerCommands(
|
|||||||
const sessions = manager.getAllSessions();
|
const sessions = manager.getAllSessions();
|
||||||
const names = Object.keys(sessions);
|
const names = Object.keys(sessions);
|
||||||
if (names.length === 0) {
|
if (names.length === 0) {
|
||||||
await ctx.reply("No active sessions.");
|
await ctx.reply("No active sessions.", { reply_markup: keyboard });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,12 +99,12 @@ export function registerCommands(
|
|||||||
return `${marker} ${name} — ${s.cwd} (${s.status})`;
|
return `${marker} ${name} — ${s.cwd} (${s.status})`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const keyboard = new InlineKeyboard();
|
const inlineKb = new InlineKeyboard();
|
||||||
for (const name of names) {
|
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) => {
|
onCommand["status"] = async (ctx) => {
|
||||||
@@ -146,37 +147,33 @@ export function registerCommands(
|
|||||||
const projects = manager.getProjects();
|
const projects = manager.getProjects();
|
||||||
const names = Object.keys(projects);
|
const names = Object.keys(projects);
|
||||||
if (names.length === 0) {
|
if (names.length === 0) {
|
||||||
await ctx.reply("No saved projects.");
|
await ctx.reply("No saved projects. Use /save <name> <path>", { reply_markup: keyboard });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = names.map((n) => `${n} — ${projects[n]}`);
|
const lines = names.map((n) => `${n} — ${projects[n]}`);
|
||||||
const keyboard = new InlineKeyboard();
|
const inlineKb = new InlineKeyboard();
|
||||||
for (const name of names) {
|
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) => {
|
onCommand["help"] = async (ctx) => {
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
[
|
[
|
||||||
"Session commands:",
|
"Use buttons below or 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",
|
|
||||||
"",
|
"",
|
||||||
"Project bookmarks:",
|
"/start <path|name> — start new session",
|
||||||
" /save <name> <path> — save bookmark",
|
"/stop [name] — stop session",
|
||||||
" /remove <name> — remove bookmark",
|
"/switch <name> — switch active session",
|
||||||
" /projects — list bookmarks",
|
"/save <name> <path> — save bookmark",
|
||||||
|
"/remove <name> — remove bookmark",
|
||||||
"",
|
"",
|
||||||
"Any other text is sent to the active session.",
|
"Any other text is sent to the active session.",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
|
{ reply_markup: keyboard },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user