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()
}