mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Strip Assistant decorative status symbols
This commit is contained in:
@@ -6826,8 +6826,8 @@
|
||||
"summary": "Align Assistant live tool progress with OpenCode source workflow",
|
||||
"target_id": "v6-product-lane-expansion",
|
||||
"claimed_at": "2026-06-06T04:02:24Z",
|
||||
"heartbeat_at": "2026-06-06T04:28:32Z",
|
||||
"expires_at": "2026-06-06T12:28:32Z",
|
||||
"heartbeat_at": "2026-06-06T04:46:07Z",
|
||||
"expires_at": "2026-06-06T12:46:07Z",
|
||||
"work_item": {
|
||||
"kind": "lane-followup",
|
||||
"id": "architecture-post-rc-canonicalization"
|
||||
|
||||
@@ -457,6 +457,15 @@ runtime cost control, and shared AI transport surfaces.
|
||||
Assistant stream events must pass through `chat.StreamEvent.ClientSafe()`;
|
||||
provider `thinking` chunks are runtime-only and may be retained internally
|
||||
for model continuity, but they are dropped before the browser/API boundary.
|
||||
The referenced OpenCode source at fetched `origin/dev` commit
|
||||
`1399323b78a04229d9bfe00c7436d7f41770fda8` stores assistant output as typed
|
||||
text, reasoning, and `tool-invocation` parts in
|
||||
`packages/opencode/src/session/message.ts` and mutates those typed parts
|
||||
through explicit stream events in
|
||||
`packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx`; Pulse adapts that
|
||||
invariant by stripping operational decorative status glyphs, warning icons,
|
||||
and check/cross badges from browser-safe assistant prose while preserving
|
||||
ordinary Unicode answer text such as units.
|
||||
The agentic stream may translate the first private provider reasoning delta
|
||||
before visible output into a neutral `model_thinking` workflow status so the
|
||||
drawer shows live activity without exposing chain-of-thought. Neutral
|
||||
|
||||
@@ -66,6 +66,35 @@ var (
|
||||
// instead of a structured tool call. Gate on canonical tool names so a
|
||||
// random prose function call is not stripped.
|
||||
plainFunctionToolCallRe = regexp.MustCompile(`(?:^|[^a-zA-Z0-9_])([a-zA-Z_][a-zA-Z0-9_]*)[ \t\r\n]*\(`)
|
||||
|
||||
// Operational providers often decorate alert/status answers with emoji
|
||||
// badges even when instructed not to. Pulse renders state through typed
|
||||
// tool/progress rows, so these glyphs are treated as presentation artifacts
|
||||
// at the browser/API boundary rather than assistant prose.
|
||||
decorativeAssistantSymbolReplacer = strings.NewReplacer(
|
||||
"\ufe0f", "",
|
||||
"⚠️", "", "⚠", "",
|
||||
"🚨", "", "🛑", "", "⛔", "",
|
||||
"🔴", "", "🟠", "", "🟡", "", "🟢", "", "🔵", "", "🟣", "", "🟤", "", "⚫", "", "⚪", "",
|
||||
"✅", "", "❌", "", "❎", "", "✔️", "", "✔", "", "☑️", "", "☑", "", "✖️", "", "✖", "", "✗", "", "✘", "",
|
||||
"ℹ️", "", "ℹ", "", "❗", "", "❕", "", "❓", "", "❔", "",
|
||||
"🔧", "", "🛠️", "", "🛠", "", "🧰", "",
|
||||
"🔥", "", "💡", "", "📌", "", "📍", "", "📊", "", "📈", "", "📉", "",
|
||||
)
|
||||
decorativeAssistantSymbols = []string{
|
||||
"⚠️", "⚠", "🚨", "🛑", "⛔",
|
||||
"🔴", "🟠", "🟡", "🟢", "🔵", "🟣", "🟤", "⚫", "⚪",
|
||||
"✅", "❌", "❎", "✔️", "✔", "☑️", "☑", "✖️", "✖", "✗", "✘",
|
||||
"ℹ️", "ℹ", "❗", "❕", "❓", "❔",
|
||||
"🔧", "🛠️", "🛠", "🧰",
|
||||
"🔥", "💡", "📌", "📍", "📊", "📈", "📉",
|
||||
}
|
||||
decorativeWhitespaceGapRe = regexp.MustCompile(`([^\s])[ \t]{2,}([^\s])`)
|
||||
decorativeSpaceBeforePunctRe = regexp.MustCompile(`[ \t]+([,.;:!?])`)
|
||||
decorativeTightHeadingRe = regexp.MustCompile(`^([ \t]{0,3}#{1,6})([^#\s])`)
|
||||
decorativeTightListMarkerRe = regexp.MustCompile(`^([ \t]*(?:[-*+]|[0-9]+[.)]))([^ \t])`)
|
||||
decorativeTightColonRe = regexp.MustCompile(`(:)([A-Z][A-Za-z])`)
|
||||
decorativeAssistantFenceStartRe = regexp.MustCompile(`^[ \t]*(?:` + "```" + `|~~~)`)
|
||||
)
|
||||
|
||||
// cleanToolCallArtifacts removes LLM-internal tool call format leakage from content.
|
||||
@@ -85,7 +114,68 @@ func cleanToolCallArtifacts(content string) string {
|
||||
content = prefix
|
||||
}
|
||||
|
||||
return content
|
||||
return cleanDecorativeAssistantSymbols(content)
|
||||
}
|
||||
|
||||
func cleanDecorativeAssistantSymbols(content string) string {
|
||||
if content == "" {
|
||||
return content
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
builder.Grow(len(content))
|
||||
|
||||
inFence := false
|
||||
lines := strings.SplitAfter(content, "\n")
|
||||
for _, segment := range lines {
|
||||
line := strings.TrimSuffix(segment, "\n")
|
||||
hasNewline := len(segment) > len(line)
|
||||
|
||||
if decorativeAssistantFenceStartRe.MatchString(line) {
|
||||
builder.WriteString(line)
|
||||
if hasNewline {
|
||||
builder.WriteByte('\n')
|
||||
}
|
||||
inFence = !inFence
|
||||
continue
|
||||
}
|
||||
if inFence {
|
||||
builder.WriteString(line)
|
||||
if hasNewline {
|
||||
builder.WriteByte('\n')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
cleaned := decorativeAssistantSymbolReplacer.Replace(line)
|
||||
if cleaned != line {
|
||||
if lineStartsWithDecorativeAssistantSymbol(line) {
|
||||
cleaned = strings.TrimLeft(cleaned, " \t")
|
||||
}
|
||||
cleaned = decorativeTightHeadingRe.ReplaceAllString(cleaned, "$1 $2")
|
||||
cleaned = decorativeTightListMarkerRe.ReplaceAllString(cleaned, "$1 $2")
|
||||
cleaned = decorativeTightColonRe.ReplaceAllString(cleaned, "$1 $2")
|
||||
cleaned = decorativeWhitespaceGapRe.ReplaceAllString(cleaned, "$1 $2")
|
||||
cleaned = decorativeSpaceBeforePunctRe.ReplaceAllString(cleaned, "$1")
|
||||
}
|
||||
|
||||
builder.WriteString(cleaned)
|
||||
if hasNewline {
|
||||
builder.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func lineStartsWithDecorativeAssistantSymbol(line string) bool {
|
||||
trimmed := strings.TrimLeft(line, " \t")
|
||||
for _, symbol := range decorativeAssistantSymbols {
|
||||
if strings.HasPrefix(trimmed, symbol) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// containsToolCallMarker checks if content contains any known LLM-internal tool call markers.
|
||||
@@ -156,13 +246,10 @@ func appendVisibleContentBeforeToolLeak(
|
||||
idx := toolCallArtifactIndex(candidate)
|
||||
if idx < 0 {
|
||||
visible, held := splitTrailingPotentialToolNamePrefix(text)
|
||||
if visible != "" {
|
||||
builder.WriteString(visible)
|
||||
}
|
||||
if pending != nil {
|
||||
*pending = held
|
||||
}
|
||||
return visible, false
|
||||
return appendSanitizedVisibleDelta(builder, visible), false
|
||||
}
|
||||
|
||||
if idx > len(existing) {
|
||||
@@ -174,7 +261,7 @@ func appendVisibleContentBeforeToolLeak(
|
||||
return "", true
|
||||
}
|
||||
visibleDelta, _ = splitTrailingPotentialToolNamePrefix(candidate[len(existing):idx])
|
||||
builder.WriteString(visibleDelta)
|
||||
visibleDelta = appendSanitizedVisibleDelta(builder, visibleDelta)
|
||||
} else if isCompactedToolPrelude(candidate[:idx]) {
|
||||
builder.Reset()
|
||||
if pending != nil {
|
||||
@@ -190,8 +277,21 @@ func flushPendingVisibleContent(builder *strings.Builder, pending *string) strin
|
||||
}
|
||||
visible := *pending
|
||||
*pending = ""
|
||||
builder.WriteString(visible)
|
||||
return visible
|
||||
return appendSanitizedVisibleDelta(builder, visible)
|
||||
}
|
||||
|
||||
func appendSanitizedVisibleDelta(builder *strings.Builder, visible string) string {
|
||||
if visible == "" {
|
||||
return ""
|
||||
}
|
||||
existing := builder.String()
|
||||
cleanedCandidate := cleanDecorativeAssistantSymbols(existing + visible)
|
||||
if !strings.HasPrefix(cleanedCandidate, existing) {
|
||||
cleanedCandidate = existing + cleanDecorativeAssistantSymbols(visible)
|
||||
}
|
||||
delta := strings.TrimPrefix(cleanedCandidate, existing)
|
||||
builder.WriteString(delta)
|
||||
return delta
|
||||
}
|
||||
|
||||
func splitTrailingPotentialToolNamePrefix(content string) (visible string, held string) {
|
||||
|
||||
@@ -122,6 +122,16 @@ func TestCleanToolCallArtifacts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanToolCallArtifactsCleansDecorativeOperationalSymbols(t *testing.T) {
|
||||
input := "### 🔴 Critical Alerts\n###⚠️Warnings\n- ⚠️ Active AI Patrol Finding\n3.✅ Backup is healthy\nCheck ⚠️ the alert, then ✅ the backup.\nNext Steps:✅Would you like me to investigate?\nTemperature is 58°C.\n\n```text\n⚠️ literal status stays inside code\n```\n"
|
||||
expected := "### Critical Alerts\n### Warnings\n- Active AI Patrol Finding\n3. Backup is healthy\nCheck the alert, then the backup.\nNext Steps: Would you like me to investigate?\nTemperature is 58°C.\n\n```text\n⚠️ literal status stays inside code\n```\n"
|
||||
|
||||
got := cleanToolCallArtifacts(input)
|
||||
if got != expected {
|
||||
t.Fatalf("cleanToolCallArtifacts() = %q, want %q", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsToolCallMarker(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -237,3 +247,41 @@ func TestAppendVisibleContentBeforeToolLeak_ClearsCompactedPreludeWhenSplit(t *t
|
||||
t.Fatalf("compacted prelude should be cleared after split leak, got delta=%q builder=%q pending=%q", delta, builder.String(), pending)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendVisibleContentBeforeToolLeak_CleansDecorativeSymbolsAcrossChunks(t *testing.T) {
|
||||
var builder strings.Builder
|
||||
var pending string
|
||||
|
||||
delta, leakFound := appendVisibleContentBeforeToolLeak(&builder, &pending, "###")
|
||||
if leakFound {
|
||||
t.Fatal("markdown heading prefix should not be treated as a tool leak")
|
||||
}
|
||||
if delta != "###" || builder.String() != "###" {
|
||||
t.Fatalf("unexpected first delta=%q builder=%q", delta, builder.String())
|
||||
}
|
||||
|
||||
delta, leakFound = appendVisibleContentBeforeToolLeak(&builder, &pending, "⚠️Critical Alerts")
|
||||
if leakFound {
|
||||
t.Fatal("decorative status glyph should not be treated as a tool leak")
|
||||
}
|
||||
if delta != " Critical Alerts" || builder.String() != "### Critical Alerts" {
|
||||
t.Fatalf("decorative heading should stream as valid markdown, got delta=%q builder=%q", delta, builder.String())
|
||||
}
|
||||
|
||||
delta, leakFound = appendVisibleContentBeforeToolLeak(&builder, &pending, "\nNext Steps:")
|
||||
if leakFound {
|
||||
t.Fatal("plain heading text should not be treated as a tool leak")
|
||||
}
|
||||
if delta != "\nNext Steps:" {
|
||||
t.Fatalf("unexpected next-steps delta=%q", delta)
|
||||
}
|
||||
|
||||
delta, leakFound = appendVisibleContentBeforeToolLeak(&builder, &pending, "✅Would you like me to investigate?")
|
||||
if leakFound {
|
||||
t.Fatal("decorative status glyph should not be treated as a tool leak")
|
||||
}
|
||||
expected := "### Critical Alerts\nNext Steps: Would you like me to investigate?"
|
||||
if delta != " Would you like me to investigate?" || builder.String() != expected {
|
||||
t.Fatalf("decorative colon spacing should stream cleanly, got delta=%q builder=%q", delta, builder.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,3 +38,26 @@ func TestStreamEventClientSafeCleansContentToolCallArtifacts(t *testing.T) {
|
||||
t.Fatalf("content text = %q, want cleaned prose", content.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamEventClientSafeCleansDecorativeOperationalSymbols(t *testing.T) {
|
||||
data, err := json.Marshal(ContentData{
|
||||
Text: "### 🔴 Critical Alerts\n- ⚠️ Active AI Patrol Finding\nTemperature is 58°C.",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
event, ok := (StreamEvent{Type: "content", Data: data}).ClientSafe()
|
||||
if !ok {
|
||||
t.Fatal("content event should remain visible")
|
||||
}
|
||||
|
||||
var content ContentData
|
||||
if err := json.Unmarshal(event.Data, &content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := "### Critical Alerts\n- Active AI Patrol Finding\nTemperature is 58°C."
|
||||
if content.Text != expected {
|
||||
t.Fatalf("content text = %q, want %q", content.Text, expected)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user