fix(telegram): show tool status without placeholder (#139)

* fix(channels): add reasoning delivery modes

* fix(telegram): show tool status without placeholder
This commit is contained in:
Duy /zuey/ authored and GitHub committed 2026-06-05 17:29:17 +07:00
1 parent 53ef912441
commit 391b6c686a
2 files changed
+155 -4

No files matched your search

+51 -4
View File
@@ -218,10 +218,7 @@ func (c *Channel) Send(ctx context.Context, msg bus.OutboundMessage) error {
// Placeholder update (e.g. LLM retry notification): edit the placeholder
// but keep it alive for the final response. Don't stop typing or cleanup.
if msg.Metadata["placeholder_update"] == "true" {
if pID, ok := c.placeholders.Load(localKey); ok {
_ = c.editMessage(ctx, chatID, pID.(int), msg.Content)
}
return nil
return c.updatePlaceholder(ctx, localKey, chatID, msg.Content, replyToMsgID, threadID)
}
// Stop thinking animation
@@ -474,6 +471,56 @@ func (c *Channel) sendHTML(ctx context.Context, chatID int64, htmlContent string
return c.sendHTMLWithDepth(ctx, chatID, htmlContent, replyTo, threadID, 0)
}
func (c *Channel) updatePlaceholder(ctx context.Context, localKey string, chatID int64, content string, replyTo, threadID int) error {
if strings.TrimSpace(content) == "" {
return nil
}
htmlContent := html.EscapeString(content)
if pID, ok := c.placeholders.Load(localKey); ok {
_ = c.editMessage(ctx, chatID, pID.(int), htmlContent)
return nil
}
msg, err := c.sendPlaceholder(ctx, chatID, htmlContent, replyTo, threadID)
if err != nil {
if newChatID := extractMigrateChatID(err); newChatID != 0 {
slog.Info("telegram: group migrated to supergroup (placeholder update)",
"old_chat_id", chatID, "new_chat_id", newChatID)
c.migrateGroupChat(ctx, chatID, newChatID)
msg, err = c.sendPlaceholder(ctx, newChatID, htmlContent, replyTo, threadID)
}
if err != nil {
return err
}
}
if msg != nil && msg.MessageID > 0 {
c.placeholders.Store(localKey, msg.MessageID)
}
return nil
}
func (c *Channel) sendPlaceholder(ctx context.Context, chatID int64, htmlContent string, replyTo, threadID int) (*telego.Message, error) {
tgMsg := tu.Message(tu.ID(chatID), htmlContent)
tgMsg.ParseMode = telego.ModeHTML
if sendThreadID := resolveThreadIDForSend(threadID); sendThreadID > 0 {
tgMsg.MessageThreadID = sendThreadID
}
if replyTo > 0 {
tgMsg.ReplyParameters = &telego.ReplyParameters{
MessageID: replyTo,
AllowSendingWithoutReply: true,
}
}
var sent *telego.Message
err := c.retrySend(ctx, "sendMessage", nil, func(ctx context.Context) error {
var sendErr error
sent, sendErr = c.bot.SendMessage(ctx, tgMsg)
return sendErr
})
return sent, err
}
func (c *Channel) sendHTMLWithDepth(ctx context.Context, chatID int64, htmlContent string, replyTo, threadID, depth int) error {
tgMsg := tu.Message(tu.ID(chatID), htmlContent)
tgMsg.ParseMode = telego.ModeHTML
@@ -0,0 +1,104 @@
package telegram
import (
"context"
"encoding/json"
"strings"
"testing"
"github.com/mymmrac/telego"
ta "github.com/mymmrac/telego/telegoapi"
"github.com/nextlevelbuilder/goclaw/internal/bus"
"github.com/nextlevelbuilder/goclaw/internal/channels"
)
func TestSendPlaceholderUpdateCreatesLazyPlaceholderWhenMissing(t *testing.T) {
caller := &recordingTelegramCaller{}
bot, err := telego.NewBot(
"123456:abcdefghijklmnopqrstuvwxyzABCDE1234",
telego.WithAPICaller(caller),
telego.WithDiscardLogger(),
)
if err != nil {
t.Fatalf("NewBot: %v", err)
}
ch := &Channel{
BaseChannel: channels.NewBaseChannel("telegram", nil, nil),
bot: bot,
}
ch.SetRunning(true)
status := bus.OutboundMessage{
Channel: "telegram",
ChatID: "123",
Content: "⚡ Running code...",
Metadata: map[string]string{
"placeholder_update": "true",
"reply_to_message_id": "77",
},
}
if err := ch.Send(context.Background(), status); err != nil {
t.Fatalf("first placeholder update: %v", err)
}
if got := caller.methodNames(); strings.Join(got, ",") != "sendMessage" {
t.Fatalf("methods after first update = %v, want sendMessage", got)
}
if _, ok := ch.placeholders.Load("123"); !ok {
t.Fatal("placeholder id was not stored after lazy send")
}
status.Content = "🔍 Searching the web..."
if err := ch.Send(context.Background(), status); err != nil {
t.Fatalf("second placeholder update: %v", err)
}
if got := caller.methodNames(); strings.Join(got, ",") != "sendMessage,editMessageText" {
t.Fatalf("methods after second update = %v, want sendMessage,editMessageText", got)
}
final := bus.OutboundMessage{
Channel: "telegram",
ChatID: "123",
Content: "Done",
Metadata: map[string]string{},
}
if err := ch.Send(context.Background(), final); err != nil {
t.Fatalf("final send: %v", err)
}
if got := caller.methodNames(); strings.Join(got, ",") != "sendMessage,editMessageText,editMessageText" {
t.Fatalf("methods after final = %v, want sendMessage,editMessageText,editMessageText", got)
}
if _, ok := ch.placeholders.Load("123"); ok {
t.Fatal("placeholder survived final answer handoff")
}
}
type recordingTelegramCaller struct {
calls []recordedTelegramCall
}
type recordedTelegramCall struct {
method string
body map[string]any
}
func (c *recordingTelegramCaller) Call(_ context.Context, url string, data *ta.RequestData) (*ta.Response, error) {
method := url[strings.LastIndex(url, "/")+1:]
body := map[string]any{}
if len(data.BodyRaw) > 0 {
_ = json.Unmarshal(data.BodyRaw, &body)
}
c.calls = append(c.calls, recordedTelegramCall{method: method, body: body})
result := json.RawMessage(`{"message_id":101,"date":0,"chat":{"id":123,"type":"private"}}`)
return &ta.Response{Ok: true, Result: result}, nil
}
func (c *recordingTelegramCaller) methodNames() []string {
names := make([]string, 0, len(c.calls))
for _, call := range c.calls {
names = append(names, call.method)
}
return names
}