Merge pull request #1 from dj-oyu/copilot/fix-chat-bubble-update-issue

fix: slash commands no longer corrupt the real-time task status bubble (Telegram)
This commit is contained in:
dj-oyu 2026-02-20 23:22:34 +09:00 committed by GitHub
commit 946a79423b
5 changed files with 93 additions and 25 deletions

View file

@ -189,6 +189,7 @@ func (al *AgentLoop) Run(ctx context.Context) error {
Channel: msg.Channel,
ChatID: msg.ChatID,
Content: response,
SkipPlaceholder: true,
})
}
continue

View file

@ -798,6 +798,57 @@ func TestResolveProvider_EmptyNameReturnsFallback(t *testing.T) {
}
}
// TestSlashCommandResponseSkipsPlaceholder verifies that slash command responses
// are published with SkipPlaceholder=true so they don't overwrite the ongoing task
// status bubble.
func TestSlashCommandResponseSkipsPlaceholder(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
Model: "test-model",
MaxTokens: 4096,
MaxToolIterations: 10,
},
},
}
msgBus := bus.NewMessageBus()
provider := &mockProvider{}
al := NewAgentLoop(cfg, msgBus, provider)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
go func() {
_ = al.Run(ctx)
}()
// Send a slash command
msgBus.PublishInbound(bus.InboundMessage{
Channel: "telegram",
SenderID: "user1",
ChatID: "chat1",
Content: "/todo",
})
// Read the outbound message
outMsg, ok := msgBus.SubscribeOutbound(ctx)
if !ok {
t.Fatal("expected outbound message from slash command")
}
if !outMsg.SkipPlaceholder {
t.Errorf("expected SkipPlaceholder=true for slash command response, got false")
}
}
func TestBuildTaskReminder_Truncation(t *testing.T) {
// Build a long message (1000 runes)
longMsg := strings.Repeat("あ", 1000)

View file

@ -15,6 +15,7 @@ type OutboundMessage struct {
ChatID string `json:"chat_id"`
Content string `json:"content"`
IsStatus bool `json:"is_status,omitempty"`
SkipPlaceholder bool `json:"skip_placeholder,omitempty"`
}
type MessageHandler func(InboundMessage) error

View file

@ -185,6 +185,16 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
return fmt.Errorf("invalid chat ID: %w", err)
}
cleanContent := sanitizeTelegramOutgoingContent(msg.Content)
chunks := utils.SplitMessage(cleanContent, telegramMaxMessageChars)
if len(chunks) == 0 {
chunks = []string{cleanContent}
}
// Slash-command responses (SkipPlaceholder=true) must not touch the
// stopThinking/placeholders state that belongs to the ongoing LLM task.
firstChunkSent := false
if !msg.SkipPlaceholder {
// Stop thinking animation
if stop, ok := c.stopThinking.Load(msg.ChatID); ok {
if cf, ok := stop.(*thinkingCancel); ok && cf != nil {
@ -193,14 +203,6 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
c.stopThinking.Delete(msg.ChatID)
}
cleanContent := sanitizeTelegramOutgoingContent(msg.Content)
chunks := utils.SplitMessage(cleanContent, telegramMaxMessageChars)
if len(chunks) == 0 {
chunks = []string{cleanContent}
}
// Try to edit placeholder
firstChunkSent := false
if pID, ok := c.placeholders.Load(msg.ChatID); ok {
c.placeholders.Delete(msg.ChatID)
editMsg := tu.EditMessageText(tu.ID(chatID), pID.(int), markdownToTelegramHTML(chunks[0]))
@ -211,6 +213,7 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
}
// Fallback to new message if edit fails
}
}
sendChunk := func(text string) error {
tgMsg := tu.Message(tu.ID(chatID), markdownToTelegramHTML(text))
@ -472,7 +475,9 @@ func (c *TelegramChannel) handleQuickCommand(ctx context.Context, message telego
"peer_id": peerID,
}
// No "Thinking..." placeholder — send directly via message bus
// No "Thinking..." placeholder — send directly via message bus.
// SkipPlaceholder=true on the outbound message prevents Send() from
// touching the ongoing task's stopThinking/placeholders state.
c.HandleMessage(fmt.Sprintf("%d", user.ID), fmt.Sprintf("%d", chatID), content, nil, metadata)
return nil
}

View file

@ -86,3 +86,13 @@ func TestDisplayWidth_EmojiIsThree(t *testing.T) {
t.Fatalf("displayWidth(stars) = %d, want 15", got)
}
}
func TestParseChatID_Plain(t *testing.T) {
id, err := parseChatID("123456789")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if id != 123456789 {
t.Errorf("expected 123456789, got %d", id)
}
}