Reduce Telegram tool message noise
This commit is contained in:
@@ -60,6 +60,25 @@ func (c *Client) SendMessage(ctx context.Context, chatID int64, text string, opt
|
|||||||
return message, nil
|
return message, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) SendChatAction(ctx context.Context, chatID int64, action string) error {
|
||||||
|
params := map[string]any{
|
||||||
|
"chat_id": chatID,
|
||||||
|
"action": action,
|
||||||
|
}
|
||||||
|
var ok bool
|
||||||
|
return c.postJSON(ctx, "sendChatAction", params, &ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SendMessageDraft(ctx context.Context, chatID int64, draftID int64, text string) error {
|
||||||
|
params := map[string]any{
|
||||||
|
"chat_id": chatID,
|
||||||
|
"draft_id": draftID,
|
||||||
|
"text": text,
|
||||||
|
}
|
||||||
|
var ok bool
|
||||||
|
return c.postJSON(ctx, "sendMessageDraft", params, &ok)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) EditMessageText(ctx context.Context, chatID int64, messageID int, text string, opts EditMessageTextOptions) (Message, error) {
|
func (c *Client) EditMessageText(ctx context.Context, chatID int64, messageID int, text string, opts EditMessageTextOptions) (Message, error) {
|
||||||
params := map[string]any{
|
params := map[string]any{
|
||||||
"chat_id": chatID,
|
"chat_id": chatID,
|
||||||
|
|||||||
@@ -45,6 +45,17 @@ type outputState struct {
|
|||||||
chatID int64
|
chatID int64
|
||||||
assistant strings.Builder
|
assistant strings.Builder
|
||||||
sentAny bool
|
sentAny bool
|
||||||
|
tools map[string]toolMessageState
|
||||||
|
workingIndicatorOff context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type toolMessageState struct {
|
||||||
|
chatID int64
|
||||||
|
messageID int
|
||||||
|
toolHTML string
|
||||||
|
approvalHTML string
|
||||||
|
approvalMarkup *InlineKeyboardMarkup
|
||||||
|
editedAt string
|
||||||
}
|
}
|
||||||
|
|
||||||
type codexThreadItemView struct {
|
type codexThreadItemView struct {
|
||||||
@@ -1039,9 +1050,9 @@ func (b *Bot) handleApprovalCallback(ctx context.Context, callback *CallbackQuer
|
|||||||
if err := b.tg.AnswerCallbackQuery(ctx, callback.ID, "Sent to Codex."); err != nil {
|
if err := b.tg.AnswerCallbackQuery(ctx, callback.ID, "Sent to Codex."); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
updated := renderApprovalHTML(approval.Kind, json.RawMessage(approval.PayloadJSON), approvalStatusLine(decision))
|
updated := b.resolveApprovalMessageHTML(approval, decision)
|
||||||
_, err = b.tg.EditMessageText(ctx, callback.Message.Chat.ID, callback.Message.MessageID, updated, EditMessageTextOptions{ParseMode: "HTML"})
|
_, err = b.tg.EditMessageText(ctx, callback.Message.Chat.ID, callback.Message.MessageID, updated, EditMessageTextOptions{ParseMode: "HTML"})
|
||||||
return err
|
return ignoreTelegramMessageNotModified(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) handleCodexEvents(ctx context.Context) {
|
func (b *Bot) handleCodexEvents(ctx context.Context) {
|
||||||
@@ -1288,7 +1299,7 @@ func (b *Bot) handleCodexNotification(ctx context.Context, event codexapp.Event)
|
|||||||
return b.flushAssistantMessage(ctx, params.ThreadID)
|
return b.flushAssistantMessage(ctx, params.ThreadID)
|
||||||
}
|
}
|
||||||
if params.ThreadID != "" {
|
if params.ThreadID != "" {
|
||||||
return b.sendOutputHTMLBlock(ctx, params.ThreadID, renderCodexItemStarted(item))
|
return b.upsertToolMessage(ctx, params.ThreadID, item.ID, renderCodexItemStarted(item))
|
||||||
}
|
}
|
||||||
case "item/agentMessage/delta":
|
case "item/agentMessage/delta":
|
||||||
var params struct {
|
var params struct {
|
||||||
@@ -1322,7 +1333,7 @@ func (b *Bot) handleCodexNotification(ctx context.Context, event codexapp.Event)
|
|||||||
return b.flushAssistantMessage(ctx, params.ThreadID)
|
return b.flushAssistantMessage(ctx, params.ThreadID)
|
||||||
}
|
}
|
||||||
if params.ThreadID != "" {
|
if params.ThreadID != "" {
|
||||||
return b.sendOutputHTMLBlock(ctx, params.ThreadID, renderCodexItemCompleted(item))
|
return b.upsertToolMessage(ctx, params.ThreadID, item.ID, renderCodexItemCompleted(item))
|
||||||
}
|
}
|
||||||
case "turn/diff/updated":
|
case "turn/diff/updated":
|
||||||
var params struct {
|
var params struct {
|
||||||
@@ -1424,9 +1435,16 @@ func (b *Bot) handleCodexServerRequest(ctx context.Context, event codexapp.Event
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
text := renderApprovalHTML(kind, event.Params, "")
|
text := renderApprovalHTML(kind, event.Params, "")
|
||||||
|
markup := approvalMarkup(approval.ID)
|
||||||
|
if msg, ok, err := b.attachApprovalToToolMessage(ctx, params.ThreadID, params.ItemID, text, markup); err != nil {
|
||||||
|
return err
|
||||||
|
} else if ok {
|
||||||
|
return b.store.UpdatePendingApprovalMessage(ctx, approval.ID, msg.Chat.ID, msg.MessageID)
|
||||||
|
}
|
||||||
|
|
||||||
msg, err := b.tg.SendMessage(ctx, thread.TelegramUserID, text, SendMessageOptions{
|
msg, err := b.tg.SendMessage(ctx, thread.TelegramUserID, text, SendMessageOptions{
|
||||||
ParseMode: "HTML",
|
ParseMode: "HTML",
|
||||||
ReplyMarkup: approvalMarkup(approval.ID),
|
ReplyMarkup: markup,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1434,16 +1452,73 @@ func (b *Bot) handleCodexServerRequest(ctx context.Context, event codexapp.Event
|
|||||||
return b.store.UpdatePendingApprovalMessage(ctx, approval.ID, msg.Chat.ID, msg.MessageID)
|
return b.store.UpdatePendingApprovalMessage(ctx, approval.ID, msg.Chat.ID, msg.MessageID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Bot) newOutputState(chatID int64) *outputState {
|
||||||
|
return &outputState{
|
||||||
|
chatID: chatID,
|
||||||
|
tools: make(map[string]toolMessageState),
|
||||||
|
workingIndicatorOff: b.startWorkingIndicator(chatID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Bot) registerOutput(threadID string, chatID int64) {
|
func (b *Bot) registerOutput(threadID string, chatID int64) {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
b.outputs[threadID] = &outputState{chatID: chatID}
|
if state := b.outputs[threadID]; state != nil && state.workingIndicatorOff != nil {
|
||||||
|
state.workingIndicatorOff()
|
||||||
|
}
|
||||||
|
b.outputs[threadID] = b.newOutputState(chatID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) clearOutput(threadID string) {
|
func (b *Bot) clearOutput(threadID string) {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
state := b.outputs[threadID]
|
||||||
delete(b.outputs, threadID)
|
delete(b.outputs, threadID)
|
||||||
|
b.mu.Unlock()
|
||||||
|
if state != nil && state.workingIndicatorOff != nil {
|
||||||
|
state.workingIndicatorOff()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) startWorkingIndicator(chatID int64) context.CancelFunc {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
draftID := time.Now().UnixNano()
|
||||||
|
go func() {
|
||||||
|
useDraft := true
|
||||||
|
sendDraft := func() bool {
|
||||||
|
return b.tg.SendMessageDraft(ctx, chatID, draftID, "") == nil
|
||||||
|
}
|
||||||
|
sendTyping := func() {
|
||||||
|
if err := b.tg.SendChatAction(ctx, chatID, "typing"); err != nil && ctx.Err() == nil {
|
||||||
|
b.logger.Printf("send typing action: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sendDraft() {
|
||||||
|
useDraft = false
|
||||||
|
sendTyping()
|
||||||
|
}
|
||||||
|
|
||||||
|
draftTicker := time.NewTicker(25 * time.Second)
|
||||||
|
typingTicker := time.NewTicker(4 * time.Second)
|
||||||
|
defer draftTicker.Stop()
|
||||||
|
defer typingTicker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-draftTicker.C:
|
||||||
|
if useDraft && !sendDraft() {
|
||||||
|
useDraft = false
|
||||||
|
sendTyping()
|
||||||
|
}
|
||||||
|
case <-typingTicker.C:
|
||||||
|
if !useDraft {
|
||||||
|
sendTyping()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) hasAssistantText(threadID string) bool {
|
func (b *Bot) hasAssistantText(threadID string) bool {
|
||||||
@@ -1513,6 +1588,182 @@ func (b *Bot) sendOutputHTMLBlock(ctx context.Context, threadID, htmlText string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s toolMessageState) html() string {
|
||||||
|
return FitHTMLMessage(addEditedAtLine(combineToolApprovalHTML(s.toolHTML, s.approvalHTML), s.editedAt), TelegramHTMLMessageLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func combineToolApprovalHTML(toolHTML, approvalHTML string) string {
|
||||||
|
toolHTML = strings.TrimSpace(toolHTML)
|
||||||
|
approvalHTML = strings.TrimSpace(approvalHTML)
|
||||||
|
switch {
|
||||||
|
case toolHTML == "":
|
||||||
|
return approvalHTML
|
||||||
|
case approvalHTML == "":
|
||||||
|
return toolHTML
|
||||||
|
default:
|
||||||
|
return toolHTML + "\n\n" + approvalHTML
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addEditedAtLine(htmlText, editedAt string) string {
|
||||||
|
htmlText = strings.TrimSpace(htmlText)
|
||||||
|
if htmlText == "" || editedAt == "" {
|
||||||
|
return htmlText
|
||||||
|
}
|
||||||
|
line := EscapeHTML("Edited at: " + editedAt)
|
||||||
|
quoteIndex := strings.Index(htmlText, "<blockquote expandable>")
|
||||||
|
if quoteIndex < 0 {
|
||||||
|
return htmlText + "\n" + line
|
||||||
|
}
|
||||||
|
summary := strings.TrimRight(htmlText[:quoteIndex], "\n")
|
||||||
|
details := strings.TrimLeft(htmlText[quoteIndex:], "\n")
|
||||||
|
if summary == "" {
|
||||||
|
return line + "\n" + details
|
||||||
|
}
|
||||||
|
return summary + "\n" + line + "\n" + details
|
||||||
|
}
|
||||||
|
|
||||||
|
func editedAtTimestamp() string {
|
||||||
|
return time.Now().UTC().Format("2006-01-02 15:04:05 MST")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) upsertToolMessage(ctx context.Context, threadID, itemID, htmlText string) error {
|
||||||
|
htmlText = strings.TrimSpace(htmlText)
|
||||||
|
if htmlText == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if itemID == "" {
|
||||||
|
return b.sendOutputHTMLBlock(ctx, threadID, htmlText)
|
||||||
|
}
|
||||||
|
if err := b.flushAssistantMessage(ctx, threadID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
chatID, err := b.outputChatID(ctx, threadID)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
state := b.outputs[threadID]
|
||||||
|
if state != nil && state.tools == nil {
|
||||||
|
state.tools = make(map[string]toolMessageState)
|
||||||
|
}
|
||||||
|
if state != nil {
|
||||||
|
tool, ok := state.tools[itemID]
|
||||||
|
if ok && tool.messageID != 0 {
|
||||||
|
tool.toolHTML = htmlText
|
||||||
|
tool.editedAt = editedAtTimestamp()
|
||||||
|
state.tools[itemID] = tool
|
||||||
|
combined := tool.html()
|
||||||
|
msgChatID := tool.chatID
|
||||||
|
msgID := tool.messageID
|
||||||
|
markup := tool.approvalMarkup
|
||||||
|
b.mu.Unlock()
|
||||||
|
_, err := b.tg.EditMessageText(ctx, msgChatID, msgID, combined, EditMessageTextOptions{ParseMode: "HTML", ReplyMarkup: markup})
|
||||||
|
if err := ignoreTelegramMessageNotModified(err); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.markOutputSent(threadID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
msg, err := b.sendHTMLMessage(ctx, chatID, htmlText, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.mu.Lock()
|
||||||
|
state = b.outputs[threadID]
|
||||||
|
if state != nil {
|
||||||
|
if state.tools == nil {
|
||||||
|
state.tools = make(map[string]toolMessageState)
|
||||||
|
}
|
||||||
|
state.tools[itemID] = toolMessageState{chatID: msg.Chat.ID, messageID: msg.MessageID, toolHTML: htmlText}
|
||||||
|
}
|
||||||
|
b.mu.Unlock()
|
||||||
|
b.markOutputSent(threadID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) attachApprovalToToolMessage(ctx context.Context, threadID, itemID, approvalHTML string, markup *InlineKeyboardMarkup) (Message, bool, error) {
|
||||||
|
approvalHTML = strings.TrimSpace(approvalHTML)
|
||||||
|
if threadID == "" || itemID == "" || approvalHTML == "" {
|
||||||
|
return Message{}, false, nil
|
||||||
|
}
|
||||||
|
if err := b.flushAssistantMessage(ctx, threadID); err != nil {
|
||||||
|
return Message{}, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
state := b.outputs[threadID]
|
||||||
|
if state == nil {
|
||||||
|
b.mu.Unlock()
|
||||||
|
return Message{}, false, nil
|
||||||
|
}
|
||||||
|
tool, ok := state.tools[itemID]
|
||||||
|
if !ok || tool.messageID == 0 {
|
||||||
|
b.mu.Unlock()
|
||||||
|
return Message{}, false, nil
|
||||||
|
}
|
||||||
|
tool.approvalHTML = approvalHTML
|
||||||
|
tool.approvalMarkup = markup
|
||||||
|
tool.editedAt = editedAtTimestamp()
|
||||||
|
state.tools[itemID] = tool
|
||||||
|
combined := tool.html()
|
||||||
|
msg := Message{MessageID: tool.messageID, Chat: Chat{ID: tool.chatID}}
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
_, err := b.tg.EditMessageText(ctx, msg.Chat.ID, msg.MessageID, combined, EditMessageTextOptions{ParseMode: "HTML", ReplyMarkup: markup})
|
||||||
|
if err := ignoreTelegramMessageNotModified(err); err != nil {
|
||||||
|
b.clearToolApproval(threadID, itemID)
|
||||||
|
b.logger.Printf("edit tool approval message %s/%s: %v", threadID, itemID, err)
|
||||||
|
return Message{}, false, nil
|
||||||
|
}
|
||||||
|
b.markOutputSent(threadID)
|
||||||
|
return msg, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) clearToolApproval(threadID, itemID string) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
if state := b.outputs[threadID]; state != nil {
|
||||||
|
if tool, ok := state.tools[itemID]; ok {
|
||||||
|
tool.approvalHTML = ""
|
||||||
|
tool.approvalMarkup = nil
|
||||||
|
state.tools[itemID] = tool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) resolveApprovalMessageHTML(approval store.PendingApproval, decision string) string {
|
||||||
|
approvalHTML := renderApprovalHTML(approval.Kind, json.RawMessage(approval.PayloadJSON), approvalStatusLine(decision))
|
||||||
|
if approval.ItemID == "" {
|
||||||
|
return approvalHTML
|
||||||
|
}
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
state := b.outputs[approval.CodexThreadID]
|
||||||
|
if state == nil {
|
||||||
|
return approvalHTML
|
||||||
|
}
|
||||||
|
tool, ok := state.tools[approval.ItemID]
|
||||||
|
if !ok || tool.messageID == 0 || tool.messageID != approval.MessageID {
|
||||||
|
return approvalHTML
|
||||||
|
}
|
||||||
|
tool.approvalHTML = approvalHTML
|
||||||
|
tool.approvalMarkup = nil
|
||||||
|
tool.editedAt = editedAtTimestamp()
|
||||||
|
state.tools[approval.ItemID] = tool
|
||||||
|
return tool.html()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ignoreTelegramMessageNotModified(err error) error {
|
||||||
|
if err != nil && strings.Contains(err.Error(), "message is not modified") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
func (b *Bot) appendAssistantDelta(ctx context.Context, threadID, delta string) error {
|
func (b *Bot) appendAssistantDelta(ctx context.Context, threadID, delta string) error {
|
||||||
if delta == "" {
|
if delta == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -1560,8 +1811,12 @@ func (b *Bot) completeTurnOutput(ctx context.Context, threadID string) error {
|
|||||||
}
|
}
|
||||||
chatID := state.chatID
|
chatID := state.chatID
|
||||||
sentAny := state.sentAny
|
sentAny := state.sentAny
|
||||||
|
workingIndicatorOff := state.workingIndicatorOff
|
||||||
delete(b.outputs, threadID)
|
delete(b.outputs, threadID)
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
|
if workingIndicatorOff != nil {
|
||||||
|
workingIndicatorOff()
|
||||||
|
}
|
||||||
|
|
||||||
if !sentAny {
|
if !sentAny {
|
||||||
_, err := b.tg.SendMessage(ctx, chatID, "Done.", SendMessageOptions{})
|
_, err := b.tg.SendMessage(ctx, chatID, "Done.", SendMessageOptions{})
|
||||||
@@ -1606,10 +1861,14 @@ func (b *Bot) sendLong(ctx context.Context, chatID int64, text string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) sendHTML(ctx context.Context, chatID int64, htmlText string) error {
|
func (b *Bot) sendHTML(ctx context.Context, chatID int64, htmlText string) error {
|
||||||
_, err := b.tg.SendMessage(ctx, chatID, htmlText, SendMessageOptions{ParseMode: "HTML"})
|
_, err := b.sendHTMLMessage(ctx, chatID, htmlText, nil)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Bot) sendHTMLMessage(ctx context.Context, chatID int64, htmlText string, markup *InlineKeyboardMarkup) (Message, error) {
|
||||||
|
return b.tg.SendMessage(ctx, chatID, htmlText, SendMessageOptions{ParseMode: "HTML", ReplyMarkup: markup})
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Bot) sendError(ctx context.Context, chatID int64, prefix string, err error) error {
|
func (b *Bot) sendError(ctx context.Context, chatID int64, prefix string, err error) error {
|
||||||
_, sendErr := b.tg.SendMessage(ctx, chatID, EscapeHTML(prefix+": "+err.Error()), SendMessageOptions{ParseMode: "HTML"})
|
_, sendErr := b.tg.SendMessage(ctx, chatID, EscapeHTML(prefix+": "+err.Error()), SendMessageOptions{ParseMode: "HTML"})
|
||||||
return sendErr
|
return sendErr
|
||||||
|
|||||||
@@ -66,6 +66,108 @@ func SummaryDetailsHTMLLimited(summary, details string, limit int) string {
|
|||||||
return SummaryDetailsHTML(summary, suffix)
|
return SummaryDetailsHTML(summary, suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FitHTMLMessage(htmlText string, limit int) string {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = TelegramHTMLMessageLimit
|
||||||
|
}
|
||||||
|
htmlText = strings.TrimSpace(htmlText)
|
||||||
|
if len([]rune(htmlText)) <= limit {
|
||||||
|
return htmlText
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = "<blockquote expandable>"
|
||||||
|
const close = "</blockquote>"
|
||||||
|
const truncatedQuote = "...[truncated]"
|
||||||
|
for len([]rune(htmlText)) > limit {
|
||||||
|
contentStart, contentEnd, content := largestBlockquoteContent(htmlText, open, close)
|
||||||
|
if contentStart < 0 {
|
||||||
|
return truncateHTMLText(htmlText, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentRunes := []rune(strings.TrimSpace(html.UnescapeString(content)))
|
||||||
|
over := len([]rune(htmlText)) - limit
|
||||||
|
keep := len(contentRunes) - over - 80
|
||||||
|
if keep < 0 {
|
||||||
|
keep = 0
|
||||||
|
}
|
||||||
|
if keep >= len(contentRunes) {
|
||||||
|
keep = len(contentRunes) - 1
|
||||||
|
}
|
||||||
|
replacementText := truncatedQuote
|
||||||
|
if keep > 0 {
|
||||||
|
replacementText = strings.TrimSpace(string(contentRunes[:keep])) + "\n" + truncatedQuote
|
||||||
|
}
|
||||||
|
replacement := EscapeHTML(replacementText)
|
||||||
|
if replacement == content {
|
||||||
|
return summaryOnlyHTML(htmlText, limit)
|
||||||
|
}
|
||||||
|
htmlText = htmlText[:contentStart] + replacement + htmlText[contentEnd:]
|
||||||
|
}
|
||||||
|
return htmlText
|
||||||
|
}
|
||||||
|
|
||||||
|
func largestBlockquoteContent(htmlText, open, close string) (int, int, string) {
|
||||||
|
bestStart := -1
|
||||||
|
bestEnd := -1
|
||||||
|
bestContent := ""
|
||||||
|
searchFrom := 0
|
||||||
|
for {
|
||||||
|
start := strings.Index(htmlText[searchFrom:], open)
|
||||||
|
if start < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
start += searchFrom
|
||||||
|
contentStart := start + len(open)
|
||||||
|
end := strings.Index(htmlText[contentStart:], close)
|
||||||
|
if end < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
end += contentStart
|
||||||
|
content := htmlText[contentStart:end]
|
||||||
|
if bestStart < 0 || len([]rune(content)) > len([]rune(bestContent)) {
|
||||||
|
bestStart = contentStart
|
||||||
|
bestEnd = end
|
||||||
|
bestContent = content
|
||||||
|
}
|
||||||
|
searchFrom = end + len(close)
|
||||||
|
}
|
||||||
|
return bestStart, bestEnd, bestContent
|
||||||
|
}
|
||||||
|
|
||||||
|
func summaryOnlyHTML(htmlText string, limit int) string {
|
||||||
|
const open = "<blockquote expandable>"
|
||||||
|
const close = "</blockquote>"
|
||||||
|
for {
|
||||||
|
start := strings.Index(htmlText, open)
|
||||||
|
if start < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
end := strings.Index(htmlText[start+len(open):], close)
|
||||||
|
if end < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
end += start + len(open) + len(close)
|
||||||
|
htmlText = strings.TrimRight(htmlText[:start], "\n") + "\n" + strings.TrimLeft(htmlText[end:], "\n")
|
||||||
|
}
|
||||||
|
return truncateHTMLText(strings.TrimSpace(htmlText), limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateHTMLText(htmlText string, limit int) string {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = TelegramHTMLMessageLimit
|
||||||
|
}
|
||||||
|
suffix := "\n...[truncated]"
|
||||||
|
runes := []rune(htmlText)
|
||||||
|
if len(runes) <= limit {
|
||||||
|
return htmlText
|
||||||
|
}
|
||||||
|
keep := limit - len([]rune(suffix))
|
||||||
|
if keep < 0 {
|
||||||
|
keep = 0
|
||||||
|
}
|
||||||
|
return string(runes[:keep]) + suffix
|
||||||
|
}
|
||||||
|
|
||||||
func ChunkText(text string, max int) []string {
|
func ChunkText(text string, max int) []string {
|
||||||
if max <= 0 {
|
if max <= 0 {
|
||||||
max = TelegramMessageLimit
|
max = TelegramMessageLimit
|
||||||
|
|||||||
@@ -101,6 +101,37 @@ func TestRenderCodexStartedItems(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToolMessageAddsEditedAtBeforeDetails(t *testing.T) {
|
||||||
|
tool := toolMessageState{
|
||||||
|
toolHTML: SummaryDetailsHTML("Tool call: command finished\nCommand: go test ./...", "full output"),
|
||||||
|
editedAt: "2026-05-21 12:34:56 UTC",
|
||||||
|
}
|
||||||
|
text := tool.html()
|
||||||
|
want := "Command: go test ./...\nEdited at: 2026-05-21 12:34:56 UTC\n<blockquote expandable>"
|
||||||
|
if !strings.Contains(text, want) {
|
||||||
|
t.Fatalf("edited timestamp not placed before details: %q", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToolMessageFitsCombinedApprovalDetails(t *testing.T) {
|
||||||
|
largeToolDetails := strings.Repeat("tool output line\n", 600)
|
||||||
|
largeApprovalDetails := strings.Repeat("approval payload line\n", 600)
|
||||||
|
tool := toolMessageState{
|
||||||
|
toolHTML: SummaryDetailsHTML("Tool call: command finished", largeToolDetails),
|
||||||
|
approvalHTML: SummaryDetailsHTML("Codex requests command approval", largeApprovalDetails),
|
||||||
|
editedAt: "2026-05-21 12:34:56 UTC",
|
||||||
|
}
|
||||||
|
text := tool.html()
|
||||||
|
if len([]rune(text)) > TelegramHTMLMessageLimit {
|
||||||
|
t.Fatalf("tool message exceeds Telegram limit: %d", len([]rune(text)))
|
||||||
|
}
|
||||||
|
for _, want := range []string{"Tool call: command finished", "Codex requests command approval", "Edited at: 2026-05-21 12:34:56 UTC", "...[truncated]"} {
|
||||||
|
if !strings.Contains(text, want) {
|
||||||
|
t.Fatalf("fitted tool message missing %q in %q", want, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResumeCallbackData(t *testing.T) {
|
func TestResumeCallbackData(t *testing.T) {
|
||||||
threadID, ok := ParseResumeThreadCallbackData(ResumeThreadCallbackData(123))
|
threadID, ok := ParseResumeThreadCallbackData(ResumeThreadCallbackData(123))
|
||||||
if !ok || threadID != 123 {
|
if !ok || threadID != 123 {
|
||||||
|
|||||||
Reference in New Issue
Block a user