diff --git a/apps/backend/go.mod b/apps/backend/go.mod index 4d1826f1ea..1ced734b7e 100644 --- a/apps/backend/go.mod +++ b/apps/backend/go.mod @@ -24,6 +24,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/superfly/sprites-go v0.0.0-20260206213632-8176adff485b github.com/tuzig/vt10x v0.0.0-20231206072048-370e51642bf7 + github.com/yuin/goldmark v1.7.13 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0 diff --git a/apps/backend/go.sum b/apps/backend/go.sum index 76405ccc3f..d7a252469a 100644 --- a/apps/backend/go.sum +++ b/apps/backend/go.sum @@ -357,6 +357,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/apps/backend/internal/task/share/gist_css.go b/apps/backend/internal/task/share/gist_css.go index c89e75a8c7..8e9fc4c65d 100644 --- a/apps/backend/internal/task/share/gist_css.go +++ b/apps/backend/internal/task/share/gist_css.go @@ -216,20 +216,42 @@ code, pre { font-family: var(--mono); } .text > * { margin: 0; } .text > * + * { margin-top: 8px; } .text p { line-height: 1.6; } +.text h1, .text h2, .text h3, .text h4, .text h5, .text h6 { + margin: 14px 0 6px; + line-height: 1.3; +} +.text h1 { font-size: 1.5rem; } +.text h2 { font-size: 1.3rem; } +.text h3 { font-size: 1.12rem; } +.text h4, .text h5, .text h6 { font-size: 1rem; } +.text ul, .text ol { margin: 8px 0; padding-left: 24px; } +.text li + li { margin-top: 3px; } +.text blockquote { + margin: 8px 0; + padding-left: 12px; + border-left: 3px solid var(--accent); + color: var(--text-dim); +} +.text blockquote > :first-child { margin-top: 0; } +.text blockquote > :last-child { margin-bottom: 0; } +.text hr { margin: 16px 0; border: 0; border-top: 1px solid var(--border-strong); } +.text table { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; } +.text th, .text td { padding: 6px 8px; border: 1px solid var(--border-strong); text-align: left; } +.text th { background: var(--surface-2); } .group-user .text { font-size: 15px; } -code.inline { +.text :not(pre) > code { background: color-mix(in oklab, var(--foreground) 8%, transparent); padding: 1px 5px; border-radius: 4px; font-size: 0.9em; } -.group-user code.inline { +.group-user .text :not(pre) > code { background: color-mix(in oklab, white 22%, transparent); color: var(--primary-foreground); } -pre.code { +.text pre { margin: 4px 0; padding: 12px 14px; border-radius: var(--radius-lg); @@ -240,7 +262,7 @@ pre.code { line-height: 1.5; color: var(--text); } -.group-user pre.code { +.group-user .text pre { background: color-mix(in oklab, black 25%, transparent); border-color: color-mix(in oklab, white 14%, transparent); color: var(--primary-foreground); diff --git a/apps/backend/internal/task/share/gist_html_test.go b/apps/backend/internal/task/share/gist_html_test.go index 46be2dc40e..fb56bbfd3c 100644 --- a/apps/backend/internal/task/share/gist_html_test.go +++ b/apps/backend/internal/task/share/gist_html_test.go @@ -125,12 +125,38 @@ func TestBuildShareHTML_InlineBackticksAndFencedCode(t *testing.T) { }, } doc := BuildShareHTML(snap) - assertContains(t, doc, `t.Cleanup`) - assertContains(t, doc, `
`)
+	assertContains(t, doc, `t.Cleanup`)
+	assertContains(t, doc, `
`)
 	assertContains(t, doc, "func TestX(t *testing.T)")
 	assertContains(t, doc, "

Done.

") } +func TestBuildShareHTML_RendersMarkdownWithoutUnsafeHTML(t *testing.T) { + t.Parallel() + snap := &Snapshot{ + Task: TaskMeta{Title: "T"}, + Messages: []Message{ + {Role: roleAssistant, Blocks: []Block{{ + Kind: blockKindText, + Text: "## Summary\n\n**Pushed** successfully.\n\n- tests passed\n- lint passed\n\n[docs](https://example.com)\n\n![tracker](https://attacker.example/pixel)\n\n[![linked tracker](https://attacker.example/pixel)](https://linked.example/target)\n\n\n\n[bad](javascript:alert('xss'))\n\n[data](data:text/html;base64,PHNjcmlwdD4=)", + }}}, + }, + } + + doc := BuildShareHTML(snap) + + assertContains(t, doc, "

Summary

") + assertContains(t, doc, "Pushed") + assertContains(t, doc, "
  • tests passed
  • ") + assertContains(t, doc, `docs`) + if strings.Contains(doc, "