diff --git a/pkg/agent/context.go b/pkg/agent/context.go index ecde7c33e..f85ac7205 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -116,7 +116,7 @@ func (cb *ContextBuilder) getIdentity() string { version := config.FormatVersion() return fmt.Sprintf( - `# picoclaw 🦞 (%s) + `# picoclaw 🦞 (%s) You are picoclaw, a helpful AI assistant. @@ -134,7 +134,9 @@ Your workspace is at: %s 3. **Memory** - When interacting with me if something seems memorable, update %s/memory/MEMORY.md -4. **Context summaries** - Conversation summaries provided as context are approximate references only. They may be incomplete or outdated. Always defer to explicit user instructions over summary content.`, +4. **Context summaries** - Conversation summaries provided as context are approximate references only. They may be incomplete or outdated. Always defer to explicit user instructions over summary content. + +5. **Telegram output** — Telegram does not support markdown tables. If you need to present tabular data, use a fenced code block or a structured list instead. Markdown pipe tables will be automatically wrapped in code blocks, but it is better to avoid them entirely.`, version, workspacePath, workspacePath, workspacePath, workspacePath, workspacePath) } diff --git a/pkg/channels/telegram/parse_markdown_to_md_v2.go b/pkg/channels/telegram/parse_markdown_to_md_v2.go index 8cae312c5..a3feadafb 100644 --- a/pkg/channels/telegram/parse_markdown_to_md_v2.go +++ b/pkg/channels/telegram/parse_markdown_to_md_v2.go @@ -5,6 +5,22 @@ import ( "strings" ) +// reTable matches a markdown pipe table (header line + separator line + at least one body row). +// The (?m) flag makes ^ match at line boundaries. +// Trailing newline(s) are deliberately excluded from the match so that blank lines +// between the table and following content are preserved. +var reTable = regexp.MustCompile(`(?m)^(\|[^\n]+\|\r?\n\|[-:\|\s]+\|\r?\n(?:\|[^\n]+\|\r?\n?)+)`) + +// wrapTablesInCodeBlocks wraps each pipe table in a fenced code block so that +// Telegram renders it with monospace alignment. This must run before any entity +// processing so the ``` delimiters are recognized as code blocks. +func wrapTablesInCodeBlocks(text string) string { + return reTable.ReplaceAllStringFunc(text, func(match string) string { + match = strings.TrimRight(match, "\r\n") + return "```\n" + match + "\n```" + }) +} + // mdV2SpecialChars are all characters that must be escaped in Telegram MarkdownV2 var mdV2SpecialChars = map[rune]bool{ '*': true, @@ -89,6 +105,9 @@ var verbatimEntities = map[string]bool{ // // Reference: https://core.telegram.org/bots/api#formatting-options func markdownToTelegramMarkdownV2(text string) string { + // 0. Wrap pipe tables in fenced code blocks so Telegram renders them legibly. + text = wrapTablesInCodeBlocks(text) + // 1. Convert Markdown headings → *escaped heading text* text = reHeading.ReplaceAllStringFunc(text, func(match string) string { sub := reHeading.FindStringSubmatch(match) diff --git a/pkg/channels/telegram/parse_markdown_to_md_v2_test.go b/pkg/channels/telegram/parse_markdown_to_md_v2_test.go index fd68a9b83..110db11e7 100644 --- a/pkg/channels/telegram/parse_markdown_to_md_v2_test.go +++ b/pkg/channels/telegram/parse_markdown_to_md_v2_test.go @@ -56,6 +56,34 @@ func Test_markdownToTelegramMarkdownV2(t *testing.T) { input: "", expected: "\\", }, + { + name: "pipe table is wrapped in code block", + input: "| Header 1 | Header 2 |\n" + + "|----------|----------|\n" + + "| Cell 1 | Cell 2 |\n" + + "| Cell 3 | Cell 4 |", + expected: "```\n" + + "| Header 1 | Header 2 |\n" + + "|----------|----------|\n" + + "| Cell 1 | Cell 2 |\n" + + "| Cell 3 | Cell 4 |\n" + + "```", + }, + { + name: "pipe table surrounded by text", + input: "Some text\n\n" + + "| A | B |\n" + + "|---|---|\n" + + "| 1 | 2 |\n\n" + + "More text", + expected: "Some text\n\n" + + "```\n" + + "| A | B |\n" + + "|---|---|\n" + + "| 1 | 2 |\n" + + "```\n" + + "More text", + }, } for _, tc := range cases { diff --git a/pkg/channels/telegram/parser_markdown_to_html.go b/pkg/channels/telegram/parser_markdown_to_html.go index 0614b6e32..9558c93be 100644 --- a/pkg/channels/telegram/parser_markdown_to_html.go +++ b/pkg/channels/telegram/parser_markdown_to_html.go @@ -14,6 +14,9 @@ func markdownToTelegramHTML(text string) string { return "" } + // Wrap pipe tables in fenced code blocks so Telegram renders them legibly. + text = wrapTablesInCodeBlocks(text) + codeBlocks := extractCodeBlocks(text) text = codeBlocks.text diff --git a/pkg/channels/telegram/parser_markdown_to_html_test.go b/pkg/channels/telegram/parser_markdown_to_html_test.go index a54a1c2c7..f33c6a62a 100644 --- a/pkg/channels/telegram/parser_markdown_to_html_test.go +++ b/pkg/channels/telegram/parser_markdown_to_html_test.go @@ -70,6 +70,16 @@ func Test_markdownToTelegramHTML(t *testing.T) { input: "```json\n{\n \"path\": \"README.md\"\n}\n```", expected: "
{\n  \"path\": \"README.md\"\n}\n
", }, + { + name: "pipe table is wrapped in code block", + input: "| Header 1 | Header 2 |\n" + + "|----------|----------|\n" + + "| Cell 1 | Cell 2 |", + expected: "
| Header 1 | Header 2 |\n" +
+				"|----------|----------|\n" +
+				"| Cell 1   | Cell 2   |\n" +
+				"
", + }, } for _, tc := range cases {