Unify approval tool message rendering

This commit is contained in:
Codex
2026-05-25 06:16:51 +00:00
parent ab5cc4fbfe
commit e9dd840111
2 changed files with 92 additions and 82 deletions

View File

@@ -1285,10 +1285,10 @@ func renderCodexItemDetailsHTML(item codexThreadItemView) string {
switch item.Type {
case "commandExecution":
appendField("CWD", item.CWD)
if command := strings.TrimSpace(item.Command); command != "" {
parts = append(parts, "<b>Command</b>", CodeBlockHTML("bash", command))
}
appendField("CWD", item.CWD)
appendInt("Exit code", item.ExitCode)
appendInt64("Duration ms", item.DurationMs)
if item.AggregatedOutput != nil && strings.TrimSpace(*item.AggregatedOutput) != "" {
@@ -1339,16 +1339,21 @@ func renderArgumentsDetailsHTML(raw json.RawMessage) []string {
return []string{"<b>Arguments</b>", CodeBlockHTML("json", compactPrettyJSON(value))}
}
parts := renderSelectedArgumentDetailsHTML(object, preferredArgumentKeys(object))
if len(parts) == 0 && len(object) > 0 {
parts = append(parts, "<b>Arguments</b>", CodeBlockHTML("json", compactPrettyJSON(object)))
}
return parts
}
func renderSelectedArgumentDetailsHTML(object map[string]any, keys []string) []string {
var parts []string
for _, key := range preferredArgumentKeys(object) {
for _, key := range keys {
part := renderArgumentFieldHTML(key, object[key])
if strings.TrimSpace(part) != "" {
parts = append(parts, part)
}
}
if len(parts) == 0 && len(object) > 0 {
parts = append(parts, "<b>Arguments</b>", CodeBlockHTML("json", compactPrettyJSON(object)))
}
return parts
}
@@ -1764,16 +1769,7 @@ func (b *Bot) handleCodexServerRequest(ctx context.Context, event codexapp.Event
}
text := renderApprovalHTML(kind, event.Params, "")
markup := approvalMarkup(approval.ID)
if msg, ok, err := b.attachApprovalToToolMessage(ctx, threadID, 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{
ParseMode: "HTML",
ReplyMarkup: markup,
})
msg, err := b.upsertApprovalMessage(ctx, thread.TelegramUserID, threadID, itemID, text, markup)
if err != nil {
return err
}
@@ -2111,42 +2107,64 @@ func (b *Bot) upsertToolMessage(ctx context.Context, threadID, itemID, htmlText
return nil
}
func (b *Bot) attachApprovalToToolMessage(ctx context.Context, threadID, itemID, approvalHTML string, markup *InlineKeyboardMarkup) (Message, bool, error) {
func (b *Bot) upsertApprovalMessage(ctx context.Context, chatID int64, threadID, itemID, approvalHTML string, markup *InlineKeyboardMarkup) (Message, error) {
approvalHTML = strings.TrimSpace(approvalHTML)
if threadID == "" || itemID == "" || approvalHTML == "" {
return Message{}, false, nil
if approvalHTML == "" {
return Message{}, errors.New("approval message is empty")
}
if threadID == "" || itemID == "" {
return b.tg.SendMessage(ctx, chatID, approvalHTML, SendMessageOptions{ParseMode: "HTML", ReplyMarkup: markup})
}
if err := b.flushAssistantMessage(ctx, threadID); err != nil {
return Message{}, false, err
return Message{}, err
}
chatID, err := b.outputChatID(ctx, threadID)
if err != nil {
return Message{}, err
}
b.mu.Lock()
state := b.outputs[threadID]
if state == nil {
b.mu.Unlock()
return Message{}, false, nil
if state != nil && state.tools == nil {
state.tools = make(map[string]toolMessageState)
}
tool, ok := state.tools[itemID]
if !ok || tool.messageID == 0 {
b.mu.Unlock()
return Message{}, false, nil
if state != nil {
tool, ok := state.tools[itemID]
if ok && tool.messageID != 0 {
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: editReplyMarkup(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{}, err
}
b.markOutputSent(threadID)
return msg, 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: editReplyMarkup(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
msg, err := b.sendHTMLMessage(ctx, chatID, approvalHTML, markup)
if err != nil {
return Message{}, 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, approvalHTML: approvalHTML, approvalMarkup: markup}
}
b.mu.Unlock()
b.markOutputSent(threadID)
return msg, true, nil
return msg, nil
}
func (b *Bot) clearToolApproval(threadID, itemID string) {
@@ -2808,12 +2826,6 @@ func renderApprovalHTML(kind string, raw json.RawMessage, status string) string
if reason, _ := params["reason"].(string); reason != "" {
lines = append(lines, "", reason)
}
for _, key := range []string{"command", "cwd", "grantRoot", "permissions", "fileChanges"} {
if value, ok := params[key]; ok {
lines = append(lines, fmt.Sprintf("%s: %s", argumentLabel(key), conciseValue(value)))
}
}
summary := strings.Join(lines, "\n")
details := renderApprovalDetailsHTML(kind, raw)
limit := TelegramHTMLMessageLimit
@@ -2832,29 +2844,7 @@ func renderApprovalDetailsHTML(kind string, raw json.RawMessage) string {
if err := json.Unmarshal(raw, &params); err != nil {
return CodeBlockHTML("json", string(raw))
}
var parts []string
appendValue := func(label string, value any) {
text, complex := argumentValueText(value)
if strings.TrimSpace(text) == "" {
return
}
if complex || strings.Contains(text, "\n") || strings.EqualFold(label, "Command") || strings.EqualFold(label, "Permissions") {
language := "text"
if strings.EqualFold(label, "Command") {
language = "bash"
} else if complex || strings.EqualFold(label, "Permissions") || looksLikeJSON(text) {
language = "json"
}
parts = append(parts, "<b>"+EscapeHTML(label)+"</b>", CodeBlockHTML(language, text))
return
}
parts = append(parts, FieldHTML(label, text))
}
for _, key := range []string{"command", "cwd", "grantRoot", "permissions", "fileChanges", "parsedCmd", "reason"} {
if value, ok := params[key]; ok {
appendValue(argumentLabel(key), value)
}
}
parts := renderSelectedArgumentDetailsHTML(params, []string{"command", "cwd", "grantRoot", "permissions", "fileChanges", "parsedCmd", "reason"})
if len(parts) == 0 {
return CodeBlockHTML("json", prettyJSON(raw))
}
@@ -2876,21 +2866,6 @@ func approvalStatusLine(decision string) string {
}
}
func conciseValue(value any) string {
text := fmt.Sprint(value)
if stringValue, ok := value.(string); ok {
text = stringValue
} else if data, err := json.Marshal(value); err == nil {
text = string(data)
}
text = strings.Join(strings.Fields(text), " ")
runes := []rune(text)
if len(runes) > 180 {
return string(runes[:180]) + "..."
}
return text
}
func prettyJSON(raw json.RawMessage) string {
if len(raw) == 0 {
return ""