From 0179d098ce89fae76538bb2fd622ac2d799f74ec Mon Sep 17 00:00:00 2001 From: chenhg5 Date: Sat, 7 Mar 2026 08:36:30 +0800 Subject: [PATCH] feat(commands): add interactive buttons for lang/model/mode commands generated by llmgit Co-Authored-By: Claude --- core/engine.go | 69 +++++++++++++++++++++++++++++++++-- platform/telegram/telegram.go | 51 ++++++++++++++++++-------- 2 files changed, 102 insertions(+), 18 deletions(-) diff --git a/core/engine.go b/core/engine.go index 1e55cbf..766651d 100644 --- a/core/engine.go +++ b/core/engine.go @@ -1811,7 +1811,20 @@ func (e *Engine) cmdLang(p Platform, msg *Message, args []string) { if len(args) == 0 { cur := e.i18n.CurrentLang() name := langDisplayName(cur) - e.reply(p, msg.ReplyCtx, e.i18n.Tf(MsgLangCurrent, name)) + text := e.i18n.Tf(MsgLangCurrent, name) + buttons := [][]ButtonOption{ + { + {Text: "English", Data: "cmd:/lang en"}, + {Text: "中文", Data: "cmd:/lang zh"}, + {Text: "繁體中文", Data: "cmd:/lang zh-TW"}, + }, + { + {Text: "日本語", Data: "cmd:/lang ja"}, + {Text: "Español", Data: "cmd:/lang es"}, + {Text: "Auto", Data: "cmd:/lang auto"}, + }, + } + e.replyWithButtons(p, msg.ReplyCtx, text, buttons) return } @@ -1896,7 +1909,26 @@ func (e *Engine) cmdModel(p Platform, msg *Message, args []string) { } sb.WriteString("\n") sb.WriteString(e.i18n.T(MsgModelUsage)) - e.reply(p, msg.ReplyCtx, sb.String()) + + // Build button rows (max 3 per row); use numeric index to stay within + // Telegram's 64-byte callback_data limit for long model names. + var buttons [][]ButtonOption + var row []ButtonOption + for i, m := range models { + label := m.Name + if m.Name == current { + label = "▶ " + label + } + row = append(row, ButtonOption{Text: label, Data: fmt.Sprintf("cmd:/model %d", i+1)}) + if len(row) >= 3 { + buttons = append(buttons, row) + row = nil + } + } + if len(row) > 0 { + buttons = append(buttons, row) + } + e.replyWithButtons(p, msg.ReplyCtx, sb.String(), buttons) return } @@ -1940,7 +1972,27 @@ func (e *Engine) cmdMode(p Platform, msg *Message, args []string) { } } sb.WriteString(e.i18n.T(MsgModeUsage)) - e.reply(p, msg.ReplyCtx, sb.String()) + + var buttons [][]ButtonOption + var row []ButtonOption + for _, m := range modes { + label := m.Name + if zhLike { + label = m.NameZh + } + if m.Key == current { + label = "▶ " + label + } + row = append(row, ButtonOption{Text: label, Data: "cmd:/mode " + m.Key}) + if len(row) >= 2 { + buttons = append(buttons, row) + row = nil + } + } + if len(row) > 0 { + buttons = append(buttons, row) + } + e.replyWithButtons(p, msg.ReplyCtx, sb.String(), buttons) return } @@ -2384,6 +2436,17 @@ func (e *Engine) reply(p Platform, replyCtx any, content string) { } } +// replyWithButtons sends a reply with inline buttons if the platform supports it, +// otherwise falls back to plain text reply. +func (e *Engine) replyWithButtons(p Platform, replyCtx any, content string, buttons [][]ButtonOption) { + if bs, ok := p.(InlineButtonSender); ok { + if err := bs.SendWithButtons(e.ctx, replyCtx, content, buttons); err == nil { + return + } + } + e.reply(p, replyCtx, content) +} + // ────────────────────────────────────────────────────────────── // /memory command // ────────────────────────────────────────────────────────────── diff --git a/platform/telegram/telegram.go b/platform/telegram/telegram.go index 7c1e6f8..4f7e00f 100644 --- a/platform/telegram/telegram.go +++ b/platform/telegram/telegram.go @@ -258,7 +258,39 @@ func (p *Platform) handleCallbackQuery(cb *tgbotapi.CallbackQuery) { answer := tgbotapi.NewCallback(cb.ID, "") p.bot.Request(answer) - // Map callback data to permission response text + userName := cb.From.UserName + if userName == "" { + userName = strings.TrimSpace(cb.From.FirstName + " " + cb.From.LastName) + } + sessionKey := fmt.Sprintf("telegram:%d:%d", chatID, cb.From.ID) + rctx := replyContext{chatID: chatID, messageID: msgID} + + // Command callbacks (cmd:/lang en, cmd:/mode yolo, etc.) + if strings.HasPrefix(data, "cmd:") { + command := strings.TrimPrefix(data, "cmd:") + + // Edit original message: append the chosen option and remove buttons + origText := cb.Message.Text + if origText == "" { + origText = "" + } + edit := tgbotapi.NewEditMessageText(chatID, msgID, origText+"\n\n> "+command) + emptyMarkup := tgbotapi.NewInlineKeyboardMarkup() + edit.ReplyMarkup = &emptyMarkup + p.bot.Send(edit) + + p.handler(p, &core.Message{ + SessionKey: sessionKey, + Platform: "telegram", + UserID: userID, + UserName: userName, + Content: command, + ReplyCtx: rctx, + }) + return + } + + // Permission callbacks (perm:allow, perm:deny, perm:allow_all) var responseText string switch data { case "perm:allow": @@ -272,7 +304,6 @@ func (p *Platform) handleCallbackQuery(cb *tgbotapi.CallbackQuery) { return } - // Edit the original message to show the choice and remove buttons choiceLabel := responseText switch data { case "perm:allow": @@ -287,29 +318,19 @@ func (p *Platform) handleCallbackQuery(cb *tgbotapi.CallbackQuery) { if origText == "" { origText = "(permission request)" } - editText := origText + "\n\n" + choiceLabel - edit := tgbotapi.NewEditMessageText(chatID, msgID, editText) + edit := tgbotapi.NewEditMessageText(chatID, msgID, origText+"\n\n"+choiceLabel) emptyMarkup := tgbotapi.NewInlineKeyboardMarkup() edit.ReplyMarkup = &emptyMarkup p.bot.Send(edit) - // Route as a regular message to the engine's permission handler - userName := cb.From.UserName - if userName == "" { - userName = strings.TrimSpace(cb.From.FirstName + " " + cb.From.LastName) - } - sessionKey := fmt.Sprintf("telegram:%d:%d", chatID, cb.From.ID) - rctx := replyContext{chatID: chatID, messageID: msgID} - - coreMsg := &core.Message{ + p.handler(p, &core.Message{ SessionKey: sessionKey, Platform: "telegram", UserID: userID, UserName: userName, Content: responseText, ReplyCtx: rctx, - } - p.handler(p, coreMsg) + }) } // isDirectedAtBot checks whether a group message is directed at this bot: