Skip to content

Commit 70805f5

Browse files
committed
Split long Telegram responses into multiple messages instead of truncating
Previously, responses >= 4096 chars were sent as a .md file, and if that failed, truncated with "(response truncated)". Now long responses are split at paragraph/line boundaries into multiple messages. File upload is used as fallback only if chunk sending fails. https://claude.ai/code/session_01VtMjyBkcnrX8AZDuPj67XT
1 parent 791dab8 commit 70805f5

1 file changed

Lines changed: 61 additions & 2 deletions

File tree

internal/telegram/handler.go

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -717,11 +717,31 @@ func (h *Handler) downloadFile(fileID string) ([]byte, error) {
717717
}
718718

719719
func (h *Handler) sendResponse(chatID int64, text string) {
720-
if len(text) >= maxMessageLen {
721-
h.sendAsFile(chatID, text)
720+
if len(text) < maxMessageLen {
721+
h.sendSingleMessage(chatID, text)
722722
return
723723
}
724724

725+
// Split long responses into multiple messages.
726+
parts := splitMessage(text, maxMessageLen-100) // leave margin for safety
727+
allOK := true
728+
for i, part := range parts {
729+
if !h.sendSingleMessage(chatID, part) {
730+
h.logger.Warn("chunk send failed, falling back to file", "chunk", i+1, "of", len(parts))
731+
allOK = false
732+
break
733+
}
734+
}
735+
if allOK {
736+
return
737+
}
738+
739+
// Fallback: send as file.
740+
h.sendAsFile(chatID, text)
741+
}
742+
743+
// sendSingleMessage sends one chunk as HTML (with plain-text fallback). Returns true on success.
744+
func (h *Handler) sendSingleMessage(chatID int64, text string) bool {
725745
htmlText := markdownToTelegramHTML(text)
726746
msg := tgbotapi.NewMessage(chatID, htmlText)
727747
msg.ParseMode = tgbotapi.ModeHTML
@@ -731,8 +751,10 @@ func (h *Handler) sendResponse(chatID int64, text string) {
731751
msg.Text = text
732752
if _, err := h.bot.Send(msg); err != nil {
733753
h.logger.Error("failed to send response", "chat_id", chatID, "err", err)
754+
return false
734755
}
735756
}
757+
return true
736758
}
737759

738760
func (h *Handler) sendAsFile(chatID int64, text string) {
@@ -753,6 +775,43 @@ func (h *Handler) sendAsFile(chatID int64, text string) {
753775
}
754776
}
755777

778+
// splitMessage splits text into chunks of at most maxLen bytes,
779+
// breaking at paragraph boundaries (\n\n), then line boundaries (\n).
780+
func splitMessage(text string, maxLen int) []string {
781+
if len(text) <= maxLen {
782+
return []string{text}
783+
}
784+
785+
var parts []string
786+
for len(text) > 0 {
787+
if len(text) <= maxLen {
788+
parts = append(parts, text)
789+
break
790+
}
791+
792+
chunk := text[:maxLen]
793+
794+
// Try to break at a paragraph boundary.
795+
if idx := strings.LastIndex(chunk, "\n\n"); idx > maxLen/4 {
796+
parts = append(parts, text[:idx])
797+
text = strings.TrimLeft(text[idx:], "\n")
798+
continue
799+
}
800+
801+
// Try to break at a line boundary.
802+
if idx := strings.LastIndex(chunk, "\n"); idx > maxLen/4 {
803+
parts = append(parts, text[:idx])
804+
text = text[idx+1:]
805+
continue
806+
}
807+
808+
// Hard break at maxLen.
809+
parts = append(parts, chunk)
810+
text = text[maxLen:]
811+
}
812+
return parts
813+
}
814+
756815
// send sends a bot-generated message with MarkdownV2 (text must be pre-escaped).
757816
func (h *Handler) send(chatID int64, text string) {
758817
msg := tgbotapi.NewMessage(chatID, text)

0 commit comments

Comments
 (0)