fix: slash command responses no longer overwrite the ongoing task status bubble

Co-authored-by: dj-oyu <68707227+dj-oyu@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-02-20 10:05:50 +00:00
parent 5e8091cc01
commit 12c6137331
4 changed files with 69 additions and 14 deletions

View file

@ -186,9 +186,10 @@ func (al *AgentLoop) Run(ctx context.Context) error {
if response, handled := al.handleCommand(ctx, msg); handled { if response, handled := al.handleCommand(ctx, msg); handled {
if response != "" { if response != "" {
al.bus.PublishOutbound(bus.OutboundMessage{ al.bus.PublishOutbound(bus.OutboundMessage{
Channel: msg.Channel, Channel: msg.Channel,
ChatID: msg.ChatID, ChatID: msg.ChatID,
Content: response, Content: response,
SkipPlaceholder: true,
}) })
} }
continue 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) { func TestBuildTaskReminder_Truncation(t *testing.T) {
// Build a long message (1000 runes) // Build a long message (1000 runes)
longMsg := strings.Repeat("あ", 1000) longMsg := strings.Repeat("あ", 1000)

View file

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

View file

@ -201,15 +201,17 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
// Try to edit placeholder // Try to edit placeholder
firstChunkSent := false firstChunkSent := false
if pID, ok := c.placeholders.Load(msg.ChatID); ok { if !msg.SkipPlaceholder {
c.placeholders.Delete(msg.ChatID) if pID, ok := c.placeholders.Load(msg.ChatID); ok {
editMsg := tu.EditMessageText(tu.ID(chatID), pID.(int), markdownToTelegramHTML(chunks[0])) c.placeholders.Delete(msg.ChatID)
editMsg.ParseMode = telego.ModeHTML editMsg := tu.EditMessageText(tu.ID(chatID), pID.(int), markdownToTelegramHTML(chunks[0]))
editMsg.ParseMode = telego.ModeHTML
if _, err = c.bot.EditMessageText(ctx, editMsg); err == nil { if _, err = c.bot.EditMessageText(ctx, editMsg); err == nil {
firstChunkSent = true firstChunkSent = true
}
// Fallback to new message if edit fails
} }
// Fallback to new message if edit fails
} }
sendChunk := func(text string) error { sendChunk := func(text string) error {