fix: render status code fences as HTML in Telegram and reduce allocations

- Add statusToHTML() to convert backtick fences to <pre> 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 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-22 04:34:16 +09:00
parent 2464f63eb7
commit b7505dcb0f
2 changed files with 53 additions and 17 deletions

View file

@ -1060,6 +1060,7 @@ const (
displayPastEntries = 3 // number of compact 1-line past entries displayPastEntries = 3 // number of compact 1-line past entries
displayErrorLines = 3 // content lines inside the error code block displayErrorLines = 3 // content lines inside the error code block
maxLatestWidth = 70 // rune limit for the latest entry command (~2 Telegram lines) 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. // 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() defer task.mu.Unlock()
var sb strings.Builder 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 != "" { if workspace != "" {
project := workspace project := workspace
if idx := strings.LastIndex(workspace, "/"); idx >= 0 { if idx := strings.LastIndex(workspace, "/"); idx >= 0 {
@ -1097,10 +1102,12 @@ func buildRichStatus(task *activeTask, isBackground bool, workspace string) stri
project = workspace[idx+1:] project = workspace[idx+1:]
} }
if project != "" { 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) --- // --- Task entries region (displayPastEntries + 4 lines) ---
entries := task.toolLog entries := task.toolLog
@ -1135,13 +1142,10 @@ func buildRichStatus(task *activeTask, isBackground bool, workspace string) stri
// Latest entry: always exactly 4 lines // Latest entry: always exactly 4 lines
if latest != nil { if latest != nil {
// Line 1-2: command (truncated to ~2 Telegram lines) // Line 1-2: command (truncated to ~2 Telegram lines)
var lb strings.Builder prefix := latest.Name
lb.WriteString(latest.Name)
if latest.ArgsSnip != "" { if latest.ArgsSnip != "" {
lb.WriteByte(' ') prefix += " " + latest.ArgsSnip
lb.WriteString(latest.ArgsSnip)
} }
prefix := lb.String()
if runes := []rune(prefix); len(runes) > maxLatestWidth { if runes := []rune(prefix); len(runes) > maxLatestWidth {
sb.WriteString(string(runes[:maxLatestWidth-1])) sb.WriteString(string(runes[:maxLatestWidth-1]))
sb.WriteString("\u2026\n") sb.WriteString("\u2026\n")
@ -1150,7 +1154,9 @@ func buildRichStatus(task *activeTask, isBackground bool, workspace string) stri
sb.WriteByte('\n') sb.WriteByte('\n')
} }
// Line 3: result // Line 3: result
fmt.Fprintf(&sb, " %s\n", latest.Result) sb.WriteString(" ")
sb.WriteString(latest.Result)
sb.WriteByte('\n')
} else { } else {
sb.WriteString("\u23F3 waiting...\n") sb.WriteString("\u23F3 waiting...\n")
sb.WriteString("\u2800\n") sb.WriteString("\u2800\n")
@ -1160,7 +1166,7 @@ func buildRichStatus(task *activeTask, isBackground bool, workspace string) stri
sb.WriteString("\u2800\n") sb.WriteString("\u2800\n")
// --- Error region (separator + code fence with displayErrorLines) --- // --- 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") sb.WriteString("```\n")
errEntry := task.lastError errEntry := task.lastError

View file

@ -179,7 +179,8 @@ func (c *TelegramChannel) EditStatus(ctx context.Context, msg bus.OutboundMessag
if !ok { if !ok {
return nil // no placeholder → nothing to edit 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) _, err = c.bot.EditMessageText(ctx, editMsg)
return err 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 // Check if we already have a message for this task
if existingMsgID, ok := c.taskStatuses.Load(msg.TaskID); ok { if existingMsgID, ok := c.taskStatuses.Load(msg.TaskID); ok {
// Edit existing task status message // 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) _, err = c.bot.EditMessageText(ctx, editMsg)
if err != nil { if err != nil {
logger.DebugCF("telegram", "EditTaskStatus edit failed", map[string]interface{}{ 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 // 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) sent, err := c.bot.SendMessage(ctx, tgMsg)
if err != nil { if err != nil {
return err return err
@ -982,9 +985,36 @@ func wrapByDisplayWidth(s string, maxWidth int) []string {
return lines return lines
} }
var htmlEscaper = strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
func escapeHTML(text string) string { func escapeHTML(text string) string {
text = strings.ReplaceAll(text, "&", "&amp;") return htmlEscaper.Replace(text)
text = strings.ReplaceAll(text, "<", "&lt;") }
text = strings.ReplaceAll(text, ">", "&gt;")
return text // statusToHTML converts status message content to Telegram HTML.
// It HTML-escapes the text and converts backtick code fences to <pre> 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("<pre>")
sb.WriteString(escapeHTML(body))
sb.WriteString("</pre>")
}
}
return sb.String()
} }