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\n\n[](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, "