From 1e3e1d48e48a8f910435a7eba788398b8eb74d51 Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Sun, 22 Feb 2026 04:34:16 +0900 Subject: [PATCH] fix: render status code fences as HTML in Telegram and reduce allocations - Add statusToHTML() to convert backtick fences to
tags with HTML escaping - Apply ParseMode=HTML to EditStatus/EditTaskStatus so code blocks render properly - Replace escapeHTML's chained ReplaceAll with single-pass strings.Replacer - Extract statusSeparator const to deduplicate separator literals - Replace fmt.Fprintf with strconv.Itoa+WriteString to avoid reflection - Remove intermediate strings.Builder for latest entry prefix Co-Authored-By: Claude Opus 4.6--- pkg/agent/loop.go | 26 +++++++++++++++--------- pkg/channels/telegram.go | 44 +++++++++++++++++++++++++++++++++------- 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 14061b966..1d37732a2 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -1060,6 +1060,7 @@ const ( displayPastEntries = 3 // number of compact 1-line past entries displayErrorLines = 3 // content lines inside the error code block maxLatestWidth = 70 // rune limit for the latest entry command (~2 Telegram lines) + statusSeparator = "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n" ) // buildRichStatus builds a fixed-height terminal-like status display. @@ -1088,7 +1089,11 @@ func buildRichStatus(task *activeTask, isBackground bool, workspace string) stri defer task.mu.Unlock() var sb strings.Builder - fmt.Fprintf(&sb, "\U0001F504 Task in progress (%d/%d)\n", task.Iteration, task.MaxIter) + sb.WriteString("\U0001F504 Task in progress (") + sb.WriteString(strconv.Itoa(task.Iteration)) + sb.WriteByte('/') + sb.WriteString(strconv.Itoa(task.MaxIter)) + sb.WriteString(")\n") if workspace != "" { project := workspace if idx := strings.LastIndex(workspace, "/"); idx >= 0 { @@ -1097,10 +1102,12 @@ func buildRichStatus(task *activeTask, isBackground bool, workspace string) stri project = workspace[idx+1:] } if project != "" { - fmt.Fprintf(&sb, "\U0001F4C2 %s\n", project) + sb.WriteString("\U0001F4C2 ") + sb.WriteString(project) + sb.WriteByte('\n') } } - sb.WriteString("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n") + sb.WriteString(statusSeparator) // --- Task entries region (displayPastEntries + 4 lines) --- entries := task.toolLog @@ -1135,13 +1142,10 @@ func buildRichStatus(task *activeTask, isBackground bool, workspace string) stri // Latest entry: always exactly 4 lines if latest != nil { // Line 1-2: command (truncated to ~2 Telegram lines) - var lb strings.Builder - lb.WriteString(latest.Name) + prefix := latest.Name if latest.ArgsSnip != "" { - lb.WriteByte(' ') - lb.WriteString(latest.ArgsSnip) + prefix += " " + latest.ArgsSnip } - prefix := lb.String() if runes := []rune(prefix); len(runes) > maxLatestWidth { sb.WriteString(string(runes[:maxLatestWidth-1])) sb.WriteString("\u2026\n") @@ -1150,7 +1154,9 @@ func buildRichStatus(task *activeTask, isBackground bool, workspace string) stri sb.WriteByte('\n') } // Line 3: result - fmt.Fprintf(&sb, " %s\n", latest.Result) + sb.WriteString(" ") + sb.WriteString(latest.Result) + sb.WriteByte('\n') } else { sb.WriteString("\u23F3 waiting...\n") sb.WriteString("\u2800\n") @@ -1160,7 +1166,7 @@ func buildRichStatus(task *activeTask, isBackground bool, workspace string) stri sb.WriteString("\u2800\n") // --- Error region (separator + code fence with displayErrorLines) --- - sb.WriteString("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n") + sb.WriteString(statusSeparator) sb.WriteString("```\n") errEntry := task.lastError diff --git a/pkg/channels/telegram.go b/pkg/channels/telegram.go index 62bbb1419..c66d6598b 100644 --- a/pkg/channels/telegram.go +++ b/pkg/channels/telegram.go @@ -179,7 +179,8 @@ func (c *TelegramChannel) EditStatus(ctx context.Context, msg bus.OutboundMessag if !ok { return nil // no placeholder → nothing to edit } - editMsg := tu.EditMessageText(tu.ID(chatID), pID.(int), msg.Content) + editMsg := tu.EditMessageText(tu.ID(chatID), pID.(int), statusToHTML(msg.Content)) + editMsg.ParseMode = telego.ModeHTML _, err = c.bot.EditMessageText(ctx, editMsg) return err } @@ -196,7 +197,8 @@ func (c *TelegramChannel) EditTaskStatus(ctx context.Context, msg bus.OutboundMe // Check if we already have a message for this task if existingMsgID, ok := c.taskStatuses.Load(msg.TaskID); ok { // Edit existing task status message - editMsg := tu.EditMessageText(tu.ID(chatID), existingMsgID.(int), msg.Content) + editMsg := tu.EditMessageText(tu.ID(chatID), existingMsgID.(int), statusToHTML(msg.Content)) + editMsg.ParseMode = telego.ModeHTML _, err = c.bot.EditMessageText(ctx, editMsg) if err != nil { logger.DebugCF("telegram", "EditTaskStatus edit failed", map[string]interface{}{ @@ -208,7 +210,8 @@ func (c *TelegramChannel) EditTaskStatus(ctx context.Context, msg bus.OutboundMe } // First task status message: send a new message and track it - tgMsg := tu.Message(tu.ID(chatID), msg.Content) + tgMsg := tu.Message(tu.ID(chatID), statusToHTML(msg.Content)) + tgMsg.ParseMode = telego.ModeHTML sent, err := c.bot.SendMessage(ctx, tgMsg) if err != nil { return err @@ -982,9 +985,36 @@ func wrapByDisplayWidth(s string, maxWidth int) []string { return lines } +var htmlEscaper = strings.NewReplacer("&", "&", "<", "<", ">", ">") + func escapeHTML(text string) string { - text = strings.ReplaceAll(text, "&", "&") - text = strings.ReplaceAll(text, "<", "<") - text = strings.ReplaceAll(text, ">", ">") - return text + return htmlEscaper.Replace(text) +} + +// statusToHTML converts status message content to Telegram HTML. +// It HTML-escapes the text and converts backtick code fences to blocks. +func statusToHTML(content string) string { + parts := strings.Split(content, "```") + if len(parts) < 3 { + return escapeHTML(content) + } + var sb strings.Builder + for i, part := range parts { + if i%2 == 0 { + sb.WriteString(escapeHTML(part)) + } else { + // Strip optional language tag on the opening line + body := part + if nl := strings.Index(body, "\n"); nl >= 0 { + tag := strings.TrimSpace(body[:nl]) + if tag == "" || !strings.ContainsAny(tag, " \t") { + body = body[nl+1:] + } + } + sb.WriteString("") + sb.WriteString(escapeHTML(body)) + sb.WriteString("") + } + } + return sb.String() }