Skip to content

Commit c046d20

Browse files
ericdalloeca-agent
andcommitted
Sanitize chat titles to strip newlines and control characters
LLM-generated titles sometimes contain leading newlines, markdown header prefixes, or other control characters that display as ^J in Emacs and similar editors. Add a sanitize-title helper that takes the first meaningful line, strips control chars and markdown headers, collapses whitespace, and truncates to 40 chars. Applied at both write points: LLM title generation and manual title update. 🤖 Generated with [eca](https://eca.dev) Co-Authored-By: eca-agent <git@eca.dev>
1 parent 309348f commit c046d20

File tree

2 files changed

+28
-9
lines changed

2 files changed

+28
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
- Chat titles now re-generate at the 3rd user message using full conversation context for more accurate titles.
6+
- Sanitize chat titles to remove newlines, control characters, and markdown header prefixes.
67

78
## 0.125.0
89

src/eca/features/chat.clj

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,22 @@
477477
(swap! bg/registry* assoc-in [:jobs job-id :notified] true))
478478
(bg/evict-notified-jobs!))))
479479

480+
(defn ^:private sanitize-title
481+
"Clean up a chat title: take first meaningful line, strip control chars,
482+
markdown header prefixes, collapse whitespace, and truncate to 40 chars."
483+
[^String s]
484+
(when s
485+
(-> (string/split s #"\n")
486+
(->> (map string/trim)
487+
(remove string/blank?)
488+
first)
489+
(or "")
490+
(string/replace #"[\x00-\x1f\x7f]" " ")
491+
(string/replace #"^#+\s*" "")
492+
(string/replace #"\s+" " ")
493+
(string/trim)
494+
(as-> t (subs t 0 (min (count t) 40))))))
495+
480496
(defn ^:private prompt-messages!
481497
"Send user messages to LLM with hook processing.
482498
source-type controls hook agent.
@@ -579,7 +595,7 @@
579595
:provider-auth provider-auth
580596
:subagent? true})]
581597
(when output-text
582-
(let [title (subs output-text 0 (min (count output-text) 40))]
598+
(let [title (sanitize-title output-text)]
583599
(swap! db* assoc-in [:chats chat-id :title] title)
584600
(lifecycle/send-content! chat-ctx :system (assoc-some {:type :metadata} :title title))
585601
(when (= :idle (get-in @db* [:chats chat-id :status]))
@@ -1198,13 +1214,14 @@
11981214
[{:keys [chat-id title]} db* messenger metrics]
11991215
(when (and (get-in @db* [:chats chat-id])
12001216
title)
1201-
(swap! db* assoc-in [:chats chat-id :title] title)
1202-
(swap! db* assoc-in [:chats chat-id :title-custom?] true)
1203-
(messenger/chat-content-received messenger
1204-
{:chat-id chat-id
1205-
:role "system"
1206-
:content {:type :metadata :title title}})
1207-
(db/update-workspaces-cache! @db* metrics))
1217+
(let [title (sanitize-title title)]
1218+
(swap! db* assoc-in [:chats chat-id :title] title)
1219+
(swap! db* assoc-in [:chats chat-id :title-custom?] true)
1220+
(messenger/chat-content-received messenger
1221+
{:chat-id chat-id
1222+
:role "system"
1223+
:content {:type :metadata :title title}})
1224+
(db/update-workspaces-cache! @db* metrics)))
12081225
{})
12091226

12101227
(defn rollback-chat
@@ -1244,6 +1261,7 @@
12441261
:messenger messenger}))
12451262
{}))
12461263

1264+
12471265
(defn ^:private find-last-message-idx
12481266
"Find the last message index matching content-id by checking both
12491267
:content-id (user messages) and [:content :id] (tool calls, etc)."
@@ -1319,4 +1337,4 @@
13191337
(lifecycle/send-content! {:messenger messenger :chat-id chat-id}
13201338
:system
13211339
{:type :text :text (str "Chat forked to: " new-title)})))
1322-
{}))
1340+
{}))

0 commit comments

Comments
 (0)