diff --git a/agents/runtime.go b/agents/runtime.go index e51b902..b047977 100644 --- a/agents/runtime.go +++ b/agents/runtime.go @@ -391,7 +391,7 @@ func (a *Agent) handleTaskEvent(ctx context.Context, msg bus.AgentMessage) { } // Send reply to Matrix room - if sendErr := a.matrix.SendText(ctx, roomID, reply); sendErr != nil { + if sendErr := a.matrix.SendMarkdown(ctx, roomID, reply); sendErr != nil { a.logger.Error("failed to send orchestrated reply to Matrix", "err", sendErr) } @@ -453,13 +453,13 @@ func (a *Agent) handleEvent(ctx context.Context, msgCtx decision.MessageContext, if handler, ok := a.commands[cmdName]; ok { a.logger.Info("command_executed", "command", cmdName) reply := handler(ctx, msgCtx) - _ = a.matrix.SendText(ctx, roomID, reply) + _ = a.matrix.SendMarkdown(ctx, roomID, reply) return } // Unknown command — never falls through to rules or LLM a.logger.Info("command_unknown", "command", msgCtx.Command) - _ = a.matrix.SendText(ctx, roomID, + _ = a.matrix.SendMarkdown(ctx, roomID, fmt.Sprintf("Comando desconocido: !%s. Usa !help para ver comandos disponibles.", msgCtx.Command)) return } diff --git a/go.mod b/go.mod index ae2914c..061ecf7 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,7 @@ require ( github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + github.com/yuin/goldmark v1.7.16 // indirect go.mau.fi/util v0.8.1 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/net v0.30.0 // indirect diff --git a/go.sum b/go.sum index 393aea5..cbe556d 100644 --- a/go.sum +++ b/go.sum @@ -110,6 +110,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= +github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.mau.fi/util v0.8.1 h1:Ga43cz6esQBYqcjZ/onRoVnYWoUwjWbsxVeJg2jOTSo= go.mau.fi/util v0.8.1/go.mod h1:T1u/rD2rzidVrBLyaUdPpZiJdP/rsyi+aTzn0D+Q6wc= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= diff --git a/shell/effects/runner.go b/shell/effects/runner.go index 254b9b3..1ccb474 100644 --- a/shell/effects/runner.go +++ b/shell/effects/runner.go @@ -22,6 +22,7 @@ type Result struct { // MatrixSender is satisfied by shell/matrix.Client. type MatrixSender interface { SendText(ctx context.Context, roomID, text string) error + SendMarkdown(ctx context.Context, roomID, markdown string) error SendTyping(ctx context.Context, roomID string, typing bool) error } @@ -65,7 +66,7 @@ func (r *Runner) executeOne(ctx context.Context, roomID string, a decision.Actio if a.Reply.ThreadID != "" { target = a.Reply.ThreadID } - err := r.matrix.SendText(ctx, target, a.Reply.Content) + err := r.matrix.SendMarkdown(ctx, target, a.Reply.Content) return Result{Action: a, Output: a.Reply.Content, Err: err} case decision.ActionKindSSH: diff --git a/shell/matrix/client.go b/shell/matrix/client.go index 57cc9b2..6c82a9c 100644 --- a/shell/matrix/client.go +++ b/shell/matrix/client.go @@ -3,6 +3,7 @@ package matrix import ( "context" + "bytes" "crypto/sha256" "encoding/hex" "fmt" @@ -12,6 +13,7 @@ import ( "path/filepath" "strings" + "github.com/yuin/goldmark" "maunium.net/go/mautrix" "maunium.net/go/mautrix/crypto" "maunium.net/go/mautrix/crypto/cryptohelper" @@ -269,17 +271,29 @@ func (c *Client) SendText(ctx context.Context, roomID, text string) error { } // SendMarkdown sends a formatted (Markdown) message to a room. +// Body contains the raw markdown (plaintext fallback per Matrix spec). +// FormattedBody contains the HTML rendered by goldmark. func (c *Client) SendMarkdown(ctx context.Context, roomID, markdown string) error { + html := mdToHTML(markdown) content := event.MessageEventContent{ MsgType: event.MsgText, Body: markdown, Format: event.FormatHTML, - FormattedBody: markdown, // mautrix can render markdown -> HTML if needed + FormattedBody: html, } _, err := c.raw.SendMessageEvent(ctx, id.RoomID(roomID), event.EventMessage, content) return err } +// mdToHTML converts a Markdown string to HTML using goldmark. +func mdToHTML(md string) string { + var buf bytes.Buffer + if err := goldmark.Convert([]byte(md), &buf); err != nil { + return md // fallback to raw markdown on error + } + return buf.String() +} + // SendReaction sends a reaction to an event. func (c *Client) SendReaction(ctx context.Context, roomID, eventID, reaction string) error { _, err := c.raw.SendReaction(ctx, id.RoomID(roomID), id.EventID(eventID), reaction) diff --git a/tools/matrix.go b/tools/matrix.go index f57200a..3c92f69 100644 --- a/tools/matrix.go +++ b/tools/matrix.go @@ -9,6 +9,7 @@ import ( // Satisfied by shell/matrix.Client. type MatrixSender interface { SendText(ctx context.Context, roomID, text string) error + SendMarkdown(ctx context.Context, roomID, markdown string) error } // NewMatrixSend creates a matrix_send tool that sends a message to a Matrix room. @@ -29,7 +30,7 @@ func NewMatrixSend(sender MatrixSender) Tool { return Result{Err: fmt.Errorf("matrix_send: room_id and message are required")} } - if err := sender.SendText(ctx, roomID, message); err != nil { + if err := sender.SendMarkdown(ctx, roomID, message); err != nil { return Result{Err: fmt.Errorf("matrix_send: %w", err)} }